Compare commits

..
Author SHA1 Message Date
J. Nick Koston a35fcc12c3 [display] Share the packed pixel read through ColorUtil
ColorUtil::read_packed<BITNESS>() lives next to to_color() so drivers
with their own blit loop (it8951 carries a copy of the same switch)
can pick it up; the comments say where the folding happens.
2026-09-17 18:01:44 -05:00
J. Nick Koston 2b894cd255 [display] Resolve the blit bitness once so channel scaling stops dividing
The per pixel fallback of draw_pixels_at() scaled every channel through
esp_scale() with a divisor derived from the runtime bitness, three real
divisions per pixel, and switched on the bitness per pixel as well. The
loop is now instantiated per bitness, so the divisor is a constant the
compiler turns into a multiply and shift with identical results, and
the 888 case scales by nothing. Same pixels drawn.
2026-09-17 17:43:00 -05:00
J. Nick Koston eab2adc9f3 [display] Test that both draw_pixels_at walks draw the same pixels
A recording display runs the row and column walks over a source with
nonzero offsets and padding for 332, 565 and 888 in both byte orders
and requires the same pixel set, plus a check that the orders differ.
2026-09-17 08:44:14 -05:00
J. Nick Koston 76b6251c6c [display] Write the draw_pixels_at pixel step once
One always-inline lambda serves both loops, comments follow the image
component's wording, and the column base is stepped like the row base.
2026-09-17 08:44:14 -05:00
J. Nick Koston cdfcb71f58 [display] Keep to_color() inlined in both draw_pixels_at loops
With two call sites GCC at -Os stopped inlining to_color(), which cost
a call and spills per pixel; flatten keeps it inlined, and the row loop
now measures faster than before the change.
2026-09-17 08:44:14 -05:00
J. Nick Koston e3a62194e5 [display] Walk columns in draw_pixels_at when the display swaps the axes
The per pixel fallback always walked rows, so on a display that swaps
x and y before writing every frame buffer write landed a row apart. It
now asks pixel_axes_swapped() once and walks columns in that case, the
same rule Image::draw() uses; the column walk is only built with PSRAM.
The source pixel decode moves into a helper shared by both loops.
2026-09-17 08:44:14 -05:00
J. Nick Koston bf5e74120f [image] Check the rotated benchmark frame against the plain one
The benchmark now draws once at each rotation and requires the rotated
frame to be the transpose of the plain one before timing, so a wrong
traversal fails the run instead of passing silently.
2026-09-17 08:43:02 -05:00
J. Nick Koston d67a78d53a [image] Keep the benchmark override working without a to_code priority 2026-09-17 08:43:02 -05:00
J. Nick Koston 24edcbc1e1 [image] Build the column walk in the host benchmark
USE_PSRAM gates it, so the rotated benchmark was running the row walk.
2026-09-17 08:43:02 -05:00
J. Nick Koston 3a460d950e [image] Only build the column walk when PSRAM is present
Without PSRAM the frame buffer is in uncached RAM where a strided write
costs nothing, so the second copy of the loops bought only flash.
Adds the transparent binary and grayscale paths to the host benchmark.
2026-09-17 08:43:02 -05:00
J. Nick Koston 1c2325da30 [epaper_spi] Keep the size accessors as they were 2026-09-17 08:43:01 -05:00
J. Nick Koston e41d3511ed [image][display] One loop body per format with the order as a template argument
for_each_pixel() holds the loop order once; the per format bodies are
always-inline lambdas, which keeps the inner loop identical to dev
(GCC at -Os otherwise leaves them out of line and calls per pixel).
Members read in the loop are copied to locals so the opaque virtual
draw_pixel_at() does not force a reload per pixel. The hub75 override
goes, its rotation is forced to 0 at codegen. The host benchmark's
frame is square so the rotated case stays in bounds.
2026-09-17 08:43:01 -05:00
J. Nick Koston eaec226add [image][display] Let the display say whether pixel writes swap the axes
Rotation alone is the wrong signal: mipi_spi with hardware rotation,
epaper_spi and it8951 with a swap_xy transform, and hub75 write the
frame buffer differently from what the rotation implies. The display
now answers through pixel_axes_swapped() and Image::draw() picks the
loop order from that. The four per-format loops collapse into one body
with the format resolved at compile time.
2026-09-17 08:43:01 -05:00
J. Nick Koston 3dea35814c [image] Follow the display rotation when choosing the loop order
A rotated display swaps x and y before the frame buffer write, so the
row walk strides the frame buffer there and a strided write costs more
than a strided read. The loop order is now a compile time template
picked from the display rotation, so both orders keep the plain loop
shape. Adds a rotated case to the host benchmark.
2026-09-17 08:42:31 -05:00
J. Nick Koston 4d130624b4 [image] Trim the draw benchmark
Own the frame buffer so nothing leaks per trial, size the pixel data
once for the widest format, and drop the manifest override the build
does not need.
2026-09-17 08:42:31 -05:00
J. Nick Koston de93f2e9ff [image] Draw rows in the outer loop so pixel reads walk memory sequentially
Image::draw() iterated columns in the outer loop over a row-major
pixel buffer, so every pixel read and every frame buffer write jumped
by a full row. Walking rows keeps both accesses sequential regardless
of image shape or display rotation. The output is unchanged.

Adds a host benchmark for the four pixel formats plus the alpha path.
2026-09-17 08:42:31 -05:00
J. Nick Koston 8ac3dba11a [esp32] Add a flash chip option that drops the unused flash vendor drivers (#19217) 2026-09-17 08:40:30 -05:00
J. Nick Koston 7d17efc15f [espidf] Emit json2 size data so the link edge is not blocked (#18848) 2026-09-17 08:35:46 -05:00
J. Nick Koston 271f85185d [sensor] Remove deprecated raw_state member (#19077) 2026-09-17 08:35:15 -05:00
J. Nick Koston 28b2a689a7 [web_server] Reduce flash used by the JSON and request helpers (#19303) 2026-09-17 08:35:02 -05:00
J. Nick Koston 40934484c6 [output] Use BinaryOutput::set_state() instead of hand written turn_on/turn_off branches (#19366) 2026-09-17 08:34:36 -05:00
J. Nick Koston 05bba3fdc2 [image] Blend grayscale alpha with Color::gradient (#19356) 2026-09-17 08:17:28 -05:00
J. Nick Koston 508c24c5e3 [esp32][rp2] Print the previous boot crash report before the logger reads it (#19351) 2026-09-17 08:17:11 -05:00
J. Nick Koston 2a8d6b69cd [rp2] Bump arduino-pico framework to 6.1.0 (#19261) 2026-09-17 08:16:41 -05:00
J. Nick Koston a7559547b2 [remote_receiver] Treat buffer size as bytes on the pulse ring targets (#19101) 2026-09-17 08:15:13 -05:00
J. Nick Koston 942322738a [remote_receiver] [remote_transmitter] Keep the RMT setup error message as a pointer to the literal (#19210) 2026-09-17 08:14:57 -05:00
J. Nick Koston 7e3d4a48d1 [modbus_controller] Remove deprecated helper shims (#19076) 2026-09-17 08:56:29 -04:00
Kevin Ahrendt 8cd22eacea [audio] Bump esp-audio-libs to v4.0.1 (#19372) 2026-09-17 08:23:55 -04:00
rexmoriarty cf0a87de28 [mixer] Don't discard a start request while reaping a stopped task (#19368) 2026-09-17 05:47:40 -04:00
Jesse Hills 4be8302190 [ci] Move skills to .agents/skills with .claude and .github symlinks (#19369) 2026-09-16 23:36:08 -05:00
Kevin Ahrendt 34158e17b8 [mixer] Raise ducking decibel_reduction maximum to 255 (#19347) 2026-09-16 20:42:29 -05:00
J. Nick Koston 1a04359454 [switch] Use Switch::control() instead of hand written turn_on/turn_off branches (#19364) 2026-09-16 19:04:14 -05:00
Kevin Ahrendt 8bcb5004da [sendspin] Add a switch platform to enable and disable the client (#19361) 2026-09-17 11:35:07 +12:00
J. Nick Koston b001514cf3 [remote_base] Accept protocols registered by external components (#19332) 2026-09-17 11:33:52 +12:00
J. Nick Koston ffb1ad288f [remote_receiver] Wake the main loop when the RMT callback stores a frame (#19102) 2026-09-17 11:32:13 +12:00
J. Nick Koston 3fe87688d9 [esp32] Enable octal flash when the flash mode is opi (#19218) 2026-09-17 11:31:27 +12:00
J. Nick Koston abfe350119 [core] Fold the modbus write and bits tests into the shared mesh fixture (#18946) 2026-09-17 11:23:29 +12:00
J. Nick Koston f8012bc467 [esp32] Redefine __FILE__ to the basename so assert paths stay out of RAM (#19106) 2026-09-17 11:22:32 +12:00
J. Nick Koston bb5c06c58a [image] Drop the unused stride_ member (#19353) 2026-09-17 11:17:56 +12:00
J. Nick Koston 9288311978 [ota] Skip the web_server plaintext warning when web_server ota is disabled (#19348) 2026-09-17 10:19:04 +12:00
J. Nick Koston 7386daed5f [ld2450] Use a user provided default constructor for MultiTargetSwitch (#19192) 2026-09-17 09:58:45 +12:00
J. Nick Koston dc300ae5d4 [esp8266] Drop the dead NEW_OOM_ABORT flag and its nothrow advice (#19247) 2026-09-17 09:58:14 +12:00
J. Nick Koston cfcaeff27b [esp32_hosted] Keep the update manifest URL as a pointer to the literal (#19212) 2026-09-17 09:57:47 +12:00
417b841db7 [icnt86] Support ICNT86 touch driver (#18331)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-09-17 07:53:53 +10:00
J. Nick Koston 711733f9af [modbus] Remove deprecated send() (#19079) 2026-09-17 09:53:43 +12:00
J. Nick Koston 2f248390b2 [modbus] Remove disable_crc validation stub (#19078) 2026-09-17 09:53:35 +12:00
J. Nick Koston 3d4765d05a [template] Remove deprecated bypass_before_arming() (#19075) 2026-09-17 09:53:28 +12:00
J. Nick Koston 59a6760f5e [nextion] Remove deprecated get_wave_chan_id() (#19080) 2026-09-17 09:53:22 +12:00
J. Nick Koston bafa096a1d [display] Trim the per pixel cost of draw_pixel_at (#19360) 2026-09-16 16:19:47 -05:00
Kevin Ahrendt 845de7b7dd [sendspin] Bump sendspin-cpp to v0.8.0 (#19357) 2026-09-16 19:38:18 +00:00
dependabot[bot] 6f15dc331f Bump prek from 0.5.2 to 0.5.3 (#19358)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-16 19:33:41 +00:00
Kevin Ahrendt fdf6998a86 [sendspin] Start mDNS service disabled, enable once server is running (#19326) 2026-09-16 15:08:40 -04:00
Kevin Ahrendt 3804ec423f [i2s_audio] Resync DMA lockstep in place instead of restarting the speaker task (#19319) 2026-09-16 13:52:56 -04:00
J. Nick Koston e13d424768 [speaker] Remove deprecated codec_support_enabled option (#19074) 2026-09-16 09:41:14 -05:00
J. Nick Koston 544f8ae771 [http_request] Take the URL and method as C strings (#19215) 2026-09-16 09:14:52 -05:00
J. Nick Koston 3a9bb7e5bc [http_request] Keep the update manifest URL as a pointer to the literal (#19211) 2026-09-16 08:41:48 -05:00
Jesse Hills 4c7aee1a77 [improv_ble] Rename from esp32_improv and decouple from ESP32 (#19264) 2026-09-16 20:49:56 +12:00
Jesse Hills 9b21b64836 Merge branch 'release' into dev 2026-09-16 20:47:35 +12:00
Jesse Hills c6e4c87e52 Merge pull request #19338 from esphome/bump-2026.9.0
2026.9.0
2026-09-16 20:47:05 +12:00
J. Nick Koston 237d87880f [pcf8574] Use a user provided default constructor for PCF8574GPIOPin (#19207) 2026-09-16 20:42:42 +12:00
J. Nick Koston dd40678ad7 [pca9554] Use a user provided default constructor for PCA9554GPIOPin (#19208) 2026-09-16 20:42:37 +12:00
J. Nick Koston 0d4c9f3243 [core] Use a user provided default constructor for Automation (#19197) 2026-09-16 20:42:32 +12:00
J. Nick Koston 0d5683525e [core] Use a user provided default constructor for DelayAction (#19137) 2026-09-16 20:42:29 +12:00
J. Nick Koston 0f01fa89b8 [api] Skip setters that pass the default port, reboot timeout and batch delay (#19227) 2026-09-16 20:42:25 +12:00
J. Nick Koston fd011ec337 [esp8266_pwm] Use a user provided default constructor for ESP8266PWM (#19198) 2026-09-16 20:42:19 +12:00
J. Nick Koston 17c5daa245 [ld2412] Drop the unused gate index from GateThresholdNumber (#19107) 2026-09-16 20:38:33 +12:00
J. Nick Koston a5931ea20e [fan] Initialize Fan::restore_mode_ (#19118) 2026-09-16 20:38:21 +12:00
J. Nick Koston 7e750ec611 [text_sensor] Use a user provided default constructor (#19112) 2026-09-16 20:38:10 +12:00
J. Nick Koston 3180381a26 [alarm_control_panel] Initialize the state members (#19120) 2026-09-16 20:38:07 +12:00
J. Nick Koston 925ddd6c09 [template] Use a user provided default constructor for TemplateEvent (#19127) 2026-09-16 20:38:00 +12:00
J. Nick Koston a495fe56f5 [template] Use a user provided default constructor for TemplateSelect (#19130) 2026-09-16 20:37:48 +12:00
J. Nick Koston cbb6600263 [template] Use a user provided default constructor for TemplateButton (#19124) 2026-09-16 20:37:41 +12:00
J. Nick Koston da73140c9c [template] Use a user provided default constructor for TemplateTextSensor (#19126) 2026-09-16 20:37:36 +12:00
J. Nick Koston b7064f2bed [update] Initialize UpdateInfo::progress (#19121) 2026-09-16 20:37:31 +12:00
J. Nick Koston 4714b77f45 [core] Construct App without value initialization (#19109) 2026-09-16 20:37:28 +12:00
J. Nick Koston 569001d554 [template] Use a user provided default constructor for TemplateSensor (#19125) 2026-09-16 20:34:07 +12:00
J. Nick Koston ac3ef0a02d [preferences] Use a user provided default constructor for IntervalSyncer (#19164) 2026-09-16 20:34:00 +12:00
J. Nick Koston 7eebe18e1a [ethernet_info] Use a user provided default constructor for IPAddressEthernetInfo (#19166) 2026-09-16 20:33:48 +12:00
J. Nick Koston ba679d91e4 [restart] Use a user provided default constructor for RestartButton (#19168) 2026-09-16 20:33:42 +12:00
J. Nick Koston 5b53c63eab [http_request] Use a user provided default constructor for HttpRequestIDF (#19175) 2026-09-16 20:33:18 +12:00
J. Nick Koston 4e0245da0d [gpio] Use a user provided default constructor for GPIOBinarySensor (#19143) 2026-09-16 20:33:13 +12:00
J. Nick Koston df4176f9e1 [uart] Use a user provided default constructor for IDFUARTComponent (#19142) 2026-09-16 20:33:08 +12:00
J. Nick Koston 25a3f7068e [status] Use a user provided default constructor for StatusBinarySensor (#19138) 2026-09-16 20:33:01 +12:00
J. Nick Koston ba50e23b81 [uptime] Use a user provided default constructor for UptimeSecondsSensor (#19140) 2026-09-16 20:32:58 +12:00
J. Nick Koston 14e640c903 [internal_temperature] Use a user provided default constructor for InternalTemperatureSensor (#19141) 2026-09-16 20:32:48 +12:00
J. Nick Koston 7f61909415 [binary_sensor] Use a user provided default constructor for SettleFilter (#19135) 2026-09-16 20:32:42 +12:00
J. Nick Koston 8ffa10f86c [version] Use a user provided default constructor for VersionTextSensor (#19139) 2026-09-16 20:32:33 +12:00
J. Nick Koston 0c1cf6d0a9 [template] Use a user provided default constructor for TemplateBinarySensor (#19128) 2026-09-16 20:32:27 +12:00
J. Nick Koston 9a76f1be4f [core] Give StaticVector a user provided default constructor (#19110) 2026-09-16 20:32:21 +12:00
J. Nick Koston 9162dc38ce [bluetooth_connection] Use a user provided default constructor for BluedroidGattClient (#19201) 2026-09-16 18:23:50 +12:00
J. Nick Koston e50428fce5 [safe_mode] Use a user provided default constructor for SafeModeComponent (#19163) 2026-09-16 18:22:34 +12:00
J. Nick Koston 511095d3da [scd4x] Use a user provided default constructor for PerformForcedCalibrationAction (#19176) 2026-09-16 18:21:58 +12:00
J. Nick Koston 2fde80454f [ld2412] Use a user provided default constructor for LightThresholdNumber (#19193) 2026-09-16 18:21:30 +12:00
J. Nick Koston 43e35c4fc2 [ld2412] Use a user provided default constructor for EngineeringModeSwitch (#19190) 2026-09-16 18:20:10 +12:00
J. Nick Koston b0f87615a2 [ld2450] Use a user provided default constructor for ZoneTypeSelect (#19188) 2026-09-16 18:19:54 +12:00
J. Nick Koston bd47f04479 [ld2450] Use a user provided default constructor for PresenceTimeoutNumber (#19195) 2026-09-16 06:19:51 +00:00
J. Nick Koston 313d0c1eb3 [number] Initialize Number::state (#19114) 2026-09-16 18:19:22 +12:00
J. Nick Koston 794aefb276 [ld2412] Use a user provided default constructor for BluetoothSwitch (#19189) 2026-09-16 18:17:17 +12:00
J. Nick Koston 4d648bf872 [ld2450] Use a user provided default constructor for BluetoothSwitch (#19191) 2026-09-16 18:17:13 +12:00
J. Nick Koston 7d1c9d6488 [ld2450] Use a user provided default constructor for BaudRateSelect (#19187) 2026-09-16 18:17:08 +12:00
J. Nick Koston e77a852599 [ld2412] Use a user provided default constructor for LightOutControlSelect (#19186) 2026-09-16 18:17:03 +12:00
J. Nick Koston 711d016a50 [ld2412] Use a user provided default constructor for BaudRateSelect (#19184) 2026-09-16 18:16:58 +12:00
J. Nick Koston d16a1ec288 [ld2412] Use a user provided default constructor for DistanceResolutionSelect (#19185) 2026-09-16 18:16:53 +12:00
J. Nick Koston 644f0ededb [ld2450] Use a user provided default constructor for FactoryResetButton (#19182) 2026-09-16 18:16:47 +12:00
J. Nick Koston 42aea9efbc [ld2450] Use a user provided default constructor for RestartButton (#19183) 2026-09-16 18:16:44 +12:00
J. Nick Koston 274bfe7aee [binary_sensor] Use a user provided default constructor (#19111) 2026-09-16 18:15:51 +12:00
J. Nick Koston 11ce3594c0 [binary_sensor] Use a user provided default constructor for DelayedOffFilter (#19206) 2026-09-16 18:10:49 +12:00
J. Nick Koston 5198ef8dd8 [binary_sensor] Use a user provided default constructor for DelayedOnFilter (#19205) 2026-09-16 18:10:43 +12:00
J. Nick Koston f686fb606b [gpio] Use a user provided default constructor for GPIOSwitch (#19203) 2026-09-16 18:10:30 +12:00
J. Nick Koston ad20711a22 [restart] Use a user provided default constructor for RestartSwitch (#19204) 2026-09-16 18:10:24 +12:00
J. Nick Koston f30a3bc3e1 [bluetooth_connection] Use a user provided default constructor for BluetoothConnection (#19200) 2026-09-16 18:10:21 +12:00
J. Nick Koston 2ffa57c907 [esp32_ble_tracker] Initialize ESPBTClient::app_id (#19199) 2026-09-16 18:07:27 +12:00
J. Nick KostonandJesse Hills f0315ea1ec [template][modbus_controller] Move set_assumed_state into the headers (#19279)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-09-16 05:38:36 +00:00
J. Nick KostonandJesse Hills d61e0e4695 [tsl2561][tsl2591] Move set_gain into the headers (#19284)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-09-16 05:26:51 +00:00
J. Nick Koston 4fba837d40 [adc] Move set_sampling_mode into the header (#19293) 2026-09-16 17:18:32 +12:00
J. Nick Koston 7ed4950c69 [bme280_base][bme680][bmp280_base] Move set_iir_filter into the headers (#19281) 2026-09-16 17:18:22 +12:00
J. Nick Koston 02435e7455 [waveshare_epaper] Move set_full_update_every into the header (#19280) 2026-09-16 17:17:12 +12:00
J. Nick Koston fdb86cbd2e [tsl2591] Move the single store setters into the header (#19288) 2026-09-16 17:16:53 +12:00
J. Nick Koston 37a08e6cb6 [lc709203f] Move the single store setters into the header (#19290) 2026-09-16 17:16:47 +12:00
J. Nick Koston d186b4ba9f [openthread] Move set_mdns into the header (#19295) 2026-09-16 17:16:42 +12:00
J. Nick Koston 1e8f5fa481 [haier] Move set_send_wifi into the header (#19299) 2026-09-16 17:16:24 +12:00
J. Nick Koston 534b4a0f44 [template] Move the cover and valve set_has_* setters into the headers (#19283) 2026-09-16 17:16:18 +12:00
J. Nick Koston 7e3a1cf272 [tsl2561] Move set_is_cs_package into the header (#19289) 2026-09-16 17:16:10 +12:00
J. Nick Koston 456119ec38 [graphical_display_menu] Move set_display and set_font into the header (#19291) 2026-09-16 17:16:05 +12:00
J. Nick Koston 031b980421 [web_server] Move the CSS and JS setters into the header (#19286) 2026-09-16 17:15:07 +12:00
J. Nick Koston cc9f4730b4 [mqtt_subscribe] Move set_qos into the headers (#19282) 2026-09-16 17:14:39 +12:00
J. Nick Koston ee6e675d58 [esp32_camera] Move the single store setters into the header (#19285) 2026-09-16 17:14:22 +12:00
J. Nick Koston 2f76ac6362 [thermostat] Move set_default_preset into the header (#19294) 2026-09-16 17:14:05 +12:00
J. Nick Koston a6fa67b7ca [bang_bang] Move the single store setters into the header (#19287) 2026-09-16 17:13:49 +12:00
J. Nick Koston da3ddee767 [st7789v] Move set_model_str into the header (#19296) 2026-09-16 17:13:34 +12:00
J. Nick Koston f95a876ef2 [max44009] Move set_mode into the header (#19297) 2026-09-16 17:13:20 +12:00
J. Nick Koston 70097ae02f [sds011] Move set_rx_mode_only into the header (#19298) 2026-09-16 17:13:06 +12:00
J. Nick Koston 1eb445bacb [template] Move set_optimistic into the headers (#19278) 2026-09-16 17:12:48 +12:00
J. Nick Koston b0996ac404 [template] Skip the switch optimistic and assumed state setters when they match the default (#19232) 2026-09-16 17:11:57 +12:00
Jesse Hills 160bc29c7c Bump version to 2026.9.0 2026-09-16 17:01:34 +12:00
Keith BurzinskiandClaude Fable 5.1 a264093d86 [uart] Add IDFUARTComponent::flush_input() and use it in uart_mux (#19336)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 21:57:06 -05:00
Jesse Hills 47be8c811f Merge branch 'beta' into dev 2026-09-16 13:38:02 +12:00
Jesse Hills b0cc913884 Merge pull request #19334 from esphome/bump-2026.9.0b5
2026.9.0b5
2026-09-16 13:37:44 +12:00
d824a32ef1 [uart_mux] New component to share a UART between a CDC-ACM bridge and local consumers (#19066)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-09-15 20:32:15 -05:00
Kevin Ahrendt ec1d8fa953 [mdns] Add runtime service enable/disable API (ESP32 only) (#19325) 2026-09-15 20:06:40 -05:00
Jesse Hills 3f7725a684 Bump version to 2026.9.0b5 2026-09-16 12:00:05 +12:00
Christopher PruijsenandJ. Nick Koston 0f500628dd [file] Keep resolved image paths as Path so config-hash normalizes them (#19267)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-09-16 12:00:03 +12:00
Bonne Eggleston fe0f04b2e4 [modbus] Add allow_broadcast_read and expect_broadcast_write_response options (#19304) 2026-09-16 12:00:03 +12:00
Edvard Filistovič 45362dbc5b [bk72xx_ble] Keep wifi power save off while BLE is compiled in (#19317) 2026-09-16 12:00:03 +12:00
J. Nick Koston a1ad794d03 [esp8266_pwm] Skip the frequency setter when it matches the default (#19224) 2026-09-16 12:00:03 +12:00
J. Nick Koston f8bda9fbad [logger] Skip the hardware UART setter when it matches the default (#19230) 2026-09-16 12:00:03 +12:00
J. Nick Koston 808b7210db [wifi] Skip setters that pass the default priority, timeouts, power save and auth mode (#19229) 2026-09-16 12:00:03 +12:00
J. Nick Koston 81be397056 [light] Skip the flash transition setter and the empty effect list (#19228) 2026-09-16 12:00:03 +12:00
J. Nick Koston eae6af437b [output] Skip the power limit setters when they match the defaults (#19225) 2026-09-16 12:00:03 +12:00
J. Nick Koston 6582c618f1 [web_server] Skip setters that pass the default port, log and include internal values (#19226) 2026-09-16 12:00:03 +12:00
J. Nick Koston 8c999d3152 [number] Fix the default mode check so mode auto is no longer emitted (#19231) 2026-09-16 12:00:03 +12:00
Jesse Hills 076dc017ab [core] Mark filters, manual_ip and interlock as advanced (#19272) 2026-09-16 12:00:03 +12:00
tronikos b7acd9c0dc [template] Stop water heater republishing when a temperature is unknown (#19013) 2026-09-16 12:00:03 +12:00
Jeff Brown 20d199ae20 [pmsa003i] Fix read from uninitialized stack memory (#19053) 2026-09-16 12:00:03 +12:00
J. Nick Koston 57860dc06c [core] Add register_simple_action and register_parented_action helpers (#19321) 2026-09-15 16:10:46 -05:00
dependabot[bot] 565dc8092c Update tzdata requirement from >=2026.3 to >=2026.4 (#19328)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-15 14:42:52 -05:00
Keith Burzinski f19403192d [usb_uart] Add claim_comm_interface option (#18969) 2026-09-15 11:47:06 -05:00
Christopher PruijsenandJ. Nick Koston 457bb3ecc9 [file] Keep resolved image paths as Path so config-hash normalizes them (#19267)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-09-15 11:33:33 -05:00
Bonne Eggleston 6362ae71c0 [modbus] Add allow_broadcast_read and expect_broadcast_write_response options (#19304) 2026-09-15 11:29:03 -05:00
Edvard Filistovič e163ae5299 [bk72xx_ble] Keep wifi power save off while BLE is compiled in (#19317) 2026-09-15 08:15:06 -05:00
esphome[bot] e737dc8dac Synchronise Device Classes from Home Assistant (#19318) 2026-09-15 07:33:24 +00:00
Kevin Ahrendt 8d61b35cdc [speaker] Make media player volume and mute independent (#19307) 2026-09-14 18:59:05 -04:00
0af1f22e77 [speaker_source] keep mute when volume changes (#18412)
Signed-off-by: carriewattsmake <carriewattsmake@users.noreply.github.com>
Co-authored-by: carriewattsmake <carriewattsmake@users.noreply.github.com>
Co-authored-by: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org>
2026-09-14 18:57:57 -04:00
7a25ca0741 [speaker_source] Don't remap a requested volume of zero (#19063)
Co-authored-by: rexmoriarty <181678468+rexmoriarty@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org>
2026-09-14 18:11:25 -04:00
Kevin Ahrendt 2472838e13 [speaker][speaker_source] Make a volume of zero silent with an audio DAC (#19305) 2026-09-14 16:46:30 -05:00
Keith BurzinskiandClaude Fable 5.1 328077c4f8 [tinyusb] Add on_mount/on_unmount triggers and is_mounted condition (#19067)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 21:16:14 +00:00
Kevin Ahrendt 588ad529e0 [i2s_audio] Ramp software volume changes (#19302) 2026-09-14 15:55:08 -05:00
dependabot[bot] 5d713ad9ad Bump filelock from 3.32.5 to 3.32.6 (#19311)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 19:34:19 +00:00
dependabot[bot] e66b590842 Bump platformdirs from 4.11.7 to 4.11.8 (#19309)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 19:34:01 +00:00
dependabot[bot]andesphome[bot] 00dfe0f712 Bump ruff from 0.16.6 to 0.16.7 (#19310)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 19:33:20 +00:00
dependabot[bot] 2c0e97421d Bump astral-sh/setup-uv from 10.0.1 to 10.1.0 (#19312)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 19:31:22 +00:00
dependabot[bot] 5d89480dba Bump github/codeql-action/analyze from 4.37.9 to 4.38.0 (#19314)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 14:30:53 -05:00
dependabot[bot] 358e240d42 Bump github/codeql-action/init from 4.37.9 to 4.38.0 (#19315)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 14:30:14 -05:00
dependabot[bot] df481eab00 Bump astral-sh/setup-uv from 10.0.1 to 10.1.0 in /.github/actions/restore-python (#19313)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-14 19:28:05 +00:00
357 changed files with 5759 additions and 2153 deletions

No files matched your search

File renamed without changes.
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+1 -1
View File
@@ -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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- 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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
+3 -3
View File
@@ -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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -413,7 +413,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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1274,7 +1274,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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -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@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+1 -1
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.6
rev: v0.16.7
hooks:
# Run the linter.
- id: ruff
+13 -2
View File
@@ -431,7 +431,17 @@ file does, and it is the authority when they disagree. The most useful starting
MyComponent *parent_;
};
```
Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. 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.
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.
* **Conditions:**
```cpp
@@ -443,7 +453,8 @@ file does, and it is the authority when they disagree. The most useful starting
MyComponent *parent_;
};
```
Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.
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.
* **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`.
+4 -1
View File
@@ -182,7 +182,6 @@ esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core
@@ -266,8 +265,10 @@ esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/iaqcore/* @yozik04
esphome/components/icnt86/* @danepowell
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
esphome/components/improv_ble/* @jesserockz
esphome/components/improv_serial/* @esphome/core
esphome/components/ina226/* @latonita @Sergio303
esphome/components/ina260/* @mreditor97
@@ -477,6 +478,7 @@ esphome/components/sendspin/image/* @kahrendt
esphome/components/sendspin/media_player/* @kahrendt
esphome/components/sendspin/media_source/* @kahrendt
esphome/components/sendspin/sensor/* @kahrendt
esphome/components/sendspin/switch/* @kahrendt
esphome/components/sendspin/text_sensor/* @kahrendt
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
@@ -589,6 +591,7 @@ esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/uart_mux/* @kbx81
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
+99 -28
View File
@@ -102,6 +102,101 @@ def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schem
return CONDITION_REGISTRY.register(name, condition_type, schema)
async def _build_with_parent(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(automation_id, template_arg, parent)
async def _build_without_parent(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
return cg.new_Pvariable(automation_id, template_arg)
async def _build_parented(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(automation_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
def register_simple_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action whose constructor takes the object named by ``config[CONF_ID]``.
Use the ``register_action`` decorator instead when the builder must also set fields.
"""
register_action(name, action_type, schema, synchronous=synchronous)(
_build_with_parent
)
def register_simple_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_simple_action``."""
register_condition(name, condition_type, schema)(_build_with_parent)
def register_bare_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action whose constructor takes no arguments."""
register_action(name, action_type, schema, synchronous=synchronous)(
_build_without_parent
)
def register_bare_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_bare_action``."""
register_condition(name, condition_type, schema)(_build_without_parent)
def register_parented_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action deriving from ``Parented<T>``.
The object is constructed without arguments and ``set_parent()`` receives the object
named by ``config[CONF_ID]``.
"""
register_action(name, action_type, schema, synchronous=synchronous)(_build_parented)
def register_parented_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_parented_action``."""
register_condition(name, condition_type, schema)(_build_parented)
Action = cg.esphome_ns.class_("Action")
Trigger = cg.esphome_ns.class_("Trigger")
ACTION_REGISTRY = Registry()
@@ -534,44 +629,20 @@ async def lambda_action_to_code(
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
@register_action(
register_simple_action(
"component.update",
UpdateComponentAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def component_update_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, comp)
@register_action(
register_simple_action(
"component.suspend",
SuspendComponentAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def component_suspend_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, comp)
@register_action(
+8 -5
View File
@@ -90,9 +90,10 @@ def get_project_cmakelists(
"""
idf_target = variant_to_idf_target(get_esp32_variant())
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
# --format=raw because the legacy mode doesn't support it.
# esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng;
# 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks
# total_size, hence the ELF fallback in espidf/size_summary.py; both
# go away together when 1.x support is dropped.
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
# Project-wide compile options: -D defines and -W warning flags (skip
@@ -211,10 +212,12 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
project({CORE.name})
# Emit raw JSON size data for ESPHome to read post-build.
# Emit per-memory-type JSON size data for ESPHome to read post-build.
# json2 stays small; raw dumps every symbol (~2s on a large map) and
# this command runs inside the link edge, blocking everything downstream.
add_custom_command(
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
${{CMAKE_PROJECT_NAME}}.map
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
+1
View File
@@ -6,5 +6,6 @@ See the component-alias section of esphome/loader.py.
# alias -> (canonical component, removal version or None)
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
"esp32_improv": ("improv_ble", "2027.4.0"),
"rp2040": ("rp2", "2027.7.0"),
}
+1 -1
View File
@@ -94,7 +94,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
/// - SamplingMode::MIN: Use the lowest sample value
/// - SamplingMode::MAX: Use the highest sample value
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
void set_sampling_mode(SamplingMode sampling_mode);
void set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
/// Perform a single ADC sampling operation and return the measured value.
/// This function handles raw readings, calibration, and averaging as needed.
@@ -76,6 +76,4 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
}
}
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
} // namespace esphome::adc
@@ -138,11 +138,11 @@ class AlarmControlPanel : public EntityBase {
// in order to store last panel state in flash
ESPPreferenceObject pref_;
// current state
AlarmControlPanelState current_state_;
AlarmControlPanelState current_state_{ACP_STATE_DISARMED};
// the desired (or previous) state
AlarmControlPanelState desired_state_;
AlarmControlPanelState desired_state_{ACP_STATE_DISARMED};
// last time the state was updated
uint32_t last_update_;
uint32_t last_update_{0};
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - passes the new state to listeners
+18 -6
View File
@@ -136,6 +136,12 @@ CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
# Schema defaults that also match the C++ initializers in api_server.h; codegen
# skips the setter when the config equals them.
DEFAULT_PORT = 6053
DEFAULT_REBOOT_TIMEOUT = "15min"
DEFAULT_BATCH_DELAY = "100ms"
def _register_provisioning_source(config: ConfigType) -> ConfigType:
"""Register the API as a provisioning source when encryption is enabled.
@@ -292,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(APIServer),
cv.Optional(CONF_PORT, default=6053): cv.port,
cv.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
# Removed in 2026.1.0 - kept to provide helpful error message
cv.Optional(CONF_PASSWORD): cv.invalid(
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
@@ -305,14 +311,14 @@ CONFIG_SCHEMA = cv.All(
"Or visit https://esphome.io/components/api/#configuration-variables"
),
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT
): cv.positive_time_period_milliseconds,
cv.Exclusive(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
cv.Optional(CONF_BATCH_DELAY, default=DEFAULT_BATCH_DELAY): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
@@ -462,9 +468,15 @@ async def to_code(config: ConfigType) -> None:
# Request a log listener slot for API log streaming
request_log_listener()
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
# Skip the setters when the config matches the C++ initializers (DEFAULT_*).
if (port := config[CONF_PORT]) != DEFAULT_PORT:
cg.add(var.set_port(port))
if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period(
DEFAULT_REBOOT_TIMEOUT
):
cg.add(var.set_reboot_timeout(reboot_timeout))
if (batch_delay := config[CONF_BATCH_DELAY]) != cv.time_period(DEFAULT_BATCH_DELAY):
cg.add(var.set_batch_delay(batch_delay))
if CONF_LISTEN_BACKLOG in config:
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
+1 -6
View File
@@ -720,12 +720,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
}
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
if (msg.state) {
a_switch->turn_on();
} else {
a_switch->turn_off();
}
a_switch->control(msg.state);
}
#endif
+3 -3
View File
@@ -314,7 +314,7 @@ class APIServer final : public Component,
#endif
// 4-byte aligned types
uint32_t reboot_timeout_{300000};
uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py
uint32_t last_connected_{0};
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
@@ -351,8 +351,8 @@ class APIServer final : public Component,
#endif
// Group smaller types together
uint16_t port_{6053};
uint16_t batch_delay_{100};
uint16_t port_{6053}; // Keep in sync with DEFAULT_PORT in __init__.py
uint16_t batch_delay_{100}; // Keep in sync with DEFAULT_BATCH_DELAY in __init__.py
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
uint8_t listen_backlog_{4};
+1 -1
View File
@@ -339,7 +339,7 @@ async def to_code(config: ConfigType) -> None:
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.0")
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
data = _get_data()
@@ -203,16 +203,10 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
this->away_config_ = away_config;
}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void BangBangClimate::dump_config() {
LOG_CLIMATE("", "Bang Bang Climate", this);
ESP_LOGCONFIG(TAG,
@@ -22,10 +22,10 @@ class BangBangClimate final : public climate::Climate, public Component {
void setup() override;
void dump_config() override;
void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
void set_supports_cool(bool supports_cool);
void set_supports_heat(bool supports_heat);
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
@@ -17,11 +17,7 @@ class BinaryLightOutput final : public light::LightOutput {
void write_state(light::LightState *state) override {
bool binary;
state->current_values_as_binary(&binary);
if (binary) {
this->output_->turn_on();
} else {
this->output_->turn_off();
}
this->output_->set_state(binary);
}
protected:
@@ -39,6 +39,7 @@ from esphome.const import (
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_GARAGE_DOOR,
DEVICE_CLASS_GAS,
DEVICE_CLASS_GLASS_BREAK,
DEVICE_CLASS_HEAT,
DEVICE_CLASS_LIGHT,
DEVICE_CLASS_LOCK,
@@ -81,6 +82,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_GARAGE_DOOR,
DEVICE_CLASS_GAS,
DEVICE_CLASS_GLASS_BREAK,
DEVICE_CLASS_HEAT,
DEVICE_CLASS_LIGHT,
DEVICE_CLASS_LOCK,
@@ -32,7 +32,8 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
*/
class BinarySensor : public StatefulEntityBase<bool> {
public:
explicit BinarySensor() = default;
// User provided, not "= default": `new(p) BinarySensor()` would zero-fill .bss that is already zero.
explicit BinarySensor() {}
const bool &get_state() const override { return this->state; }
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
@@ -53,6 +53,9 @@ class DelayedOnOffFilter final : public Filter {
class DelayedOnFilter : public Filter {
public:
// User provided, not "= default": `new(p) DelayedOnFilter()` would zero-fill .bss that is already zero.
DelayedOnFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
@@ -63,6 +66,9 @@ class DelayedOnFilter : public Filter {
class DelayedOffFilter : public Filter {
public:
// User provided, not "= default": `new(p) DelayedOffFilter()` would zero-fill .bss that is already zero.
DelayedOffFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
@@ -143,6 +149,8 @@ class StatelessLambdaFilter : public Filter {
class SettleFilter : public Filter {
public:
// User provided, not "= default": `new(p) SettleFilter()` would zero-fill .bss that is already zero.
SettleFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
+10 -1
View File
@@ -23,7 +23,7 @@ public ble_api.h.
import logging
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components import libretiny, wifi
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
@@ -84,6 +84,15 @@ def _final_validate(config: ConfigType) -> None:
# which run on a BLE 4.2 board. The hard error is raised at codegen.
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
_LOGGER.warning("%s (this configuration cannot compile)", msg)
# Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU
# sleep. With the BLE controller running, that sleep never wakes up once the
# station is stopped (adapter restart after failed roams, wifi.disable): the
# device is dead until a power cycle (esphome#18592). Keep power save off
# until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414).
wifi.force_power_save_off(
"with BLE running, the Beken SDK's MCU sleep halts the device once the "
"station is stopped (https://github.com/esphome/esphome/issues/18592)"
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -31,6 +31,9 @@ class BluetoothConnection;
// void disconnect() cannot overload with an int-returning twin.
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
public:
// User provided, not "= default": `new(p) BluedroidGattClient()` would zero-fill .bss that is already zero.
BluedroidGattClient() {}
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
// Lifecycle of one connection attempt's service search.
@@ -37,6 +37,9 @@ enum class PendingAck : uint8_t {
class BluetoothConnection final : public ble_device_base::GattClientListener {
public:
// User provided, not "= default": `new(p) BluetoothConnection()` would zero-fill .bss that is already zero.
BluetoothConnection() {}
/// Wire the platform backend. Called from codegen before setup.
void set_backend(ble_device_base::BLEGattConnection *backend) {
this->backend_ = backend;
@@ -626,7 +626,7 @@ void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
// explicit kick the MTU would only be exchanged on the first GATT query,
// which never happens on a V3_WITH_CACHE connection.
// Both registration calls above return void (BTstack 075a078, arduino-pico
// 6.0.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
// 6.1.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
// the connect timeout in loop().
gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
}
@@ -341,7 +341,6 @@ void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over
void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) {
this->humidity_oversampling_ = humidity_over_sampling;
}
void BME280Component::set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
uint8_t BME280Component::read_u8_(uint8_t a_register) {
uint8_t data = 0;
this->read_byte(a_register, &data);
+1 -1
View File
@@ -69,7 +69,7 @@ class BME280Component : public PollingComponent {
/// Set the oversampling value for the humidity sensor. Default is 16x.
void set_humidity_oversampling(BME280Oversampling humidity_over_sampling);
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
void set_iir_filter(BME280IIRFilter iir_filter);
void set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
-1
View File
@@ -503,7 +503,6 @@ void BME680Component::set_pressure_oversampling(BME680Oversampling pressure_over
void BME680Component::set_humidity_oversampling(BME680Oversampling humidity_oversampling) {
this->humidity_oversampling_ = humidity_oversampling;
}
void BME680Component::set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_duration) {
this->heater_temperature_ = heater_temperature;
this->heater_duration_ = heater_duration;
+1 -1
View File
@@ -74,7 +74,7 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice {
/// Set the humidity oversampling value. Defaults to 16X.
void set_humidity_oversampling(BME680Oversampling humidity_oversampling);
/// Set the IIR Filter value. Defaults to no IIR Filter.
void set_iir_filter(BME680IIRFilter iir_filter);
void set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
@@ -254,7 +254,6 @@ void BMP280Component::set_temperature_oversampling(BMP280Oversampling temperatur
void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over_sampling) {
this->pressure_oversampling_ = pressure_over_sampling;
}
void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
uint8_t BMP280Component::read_u8_(uint8_t a_register) {
uint8_t data = 0;
this->bmp_read_byte(a_register, &data);
+1 -1
View File
@@ -59,7 +59,7 @@ class BMP280Component : public PollingComponent {
/// Set the oversampling value for the pressure sensor. Default is 16x.
void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling);
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
void set_iir_filter(BMP280IIRFilter iir_filter);
void set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void setup() override;
void dump_config() override;
@@ -30,6 +30,7 @@ class CDCACMUARTBridge final : public Component {
void set_line_coding();
void set_line_state(bool dtr, bool rts);
uart::IDFUARTComponent *get_uart_parent() const { return this->uart_parent_; }
/**
* Stop forwarding in both directions and hand the UART back to its configured
@@ -13,12 +13,6 @@ void CopySwitch::setup() {
void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); }
void CopySwitch::write_state(bool state) {
if (state) {
source_->turn_on();
} else {
source_->turn_off();
}
}
void CopySwitch::write_state(bool state) { this->source_->control(state); }
} // namespace esphome::copy
+57 -28
View File
@@ -3,7 +3,9 @@
#include <utility>
#include <numbers>
#include "display_color_utils.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::display {
@@ -51,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
this->line(x1, y1, x2, y2, color);
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
uint32_t color_value;
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
// -Os stops inlining it once there are two call sites, which costs a call and
// spills per pixel. Without PSRAM there is one call site and it changes
// nothing, and draw_pixel_at() stays a virtual call either way.
template<ColorBitness BITNESS>
static void __attribute__((flatten))
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
bool big_endian, int x_offset, int y_offset, int x_pad) {
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
// BITNESS is a constant here, so to_color()'s three per pixel divisions
// become a multiply and shift and its bitness switch folds.
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
display->draw_pixel_at(
x + x_start, y + y_start,
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
};
// The source and frame buffers are row-major, so walking rows keeps the read
// and the write sequential. When the display swaps the axes before writing,
// walking columns keeps the write sequential instead; with the frame buffer
// in PSRAM a strided write costs more than a strided read.
#ifdef USE_PSRAM
if (display->pixel_axes_swapped()) {
size_t column_idx = y_offset * line_stride + x_offset;
for (int x = 0; x != w; x++, column_idx++) {
size_t source_idx = column_idx;
for (int y = 0; y != h; y++, source_idx += line_stride) {
draw_source_pixel(x, y, source_idx);
}
}
return;
}
#endif
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
size_t source_idx_mod;
for (int x = 0; x != w; x++, source_idx++) {
switch (bitness) {
default:
color_value = ptr[source_idx];
break;
case COLOR_BITNESS_565:
source_idx_mod = source_idx * 2;
if (big_endian) {
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
} else {
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
}
break;
case COLOR_BITNESS_888:
source_idx_mod = source_idx * 3;
if (big_endian) {
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
} else {
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
}
break;
}
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
draw_source_pixel(x, y, source_idx);
}
}
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
switch (bitness) {
case COLOR_BITNESS_565:
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
case COLOR_BITNESS_888:
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
default:
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
}
}
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
for (int i = x; i < x + width; i++)
@@ -770,10 +797,12 @@ Rect Display::get_clipping() const {
void Display::clear_clipping_() { this->clipping_rectangle_.clear(); }
void Display::feed_wdt_pixel_slow_() { App.feed_wdt(); }
bool Display::clip(int x, int y) {
if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height())
return false;
if (!this->get_clipping().inside(x, y))
if (this->is_point_clipped(x, y))
return false;
return true;
}
+30
View File
@@ -137,6 +137,11 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270,
};
/// Whether a rotation swaps the x and y axes.
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
}
const int EDGES_TRIGON = 3;
const int EDGES_TRIANGLE = 3;
const int EDGES_TETRAGON = 4;
@@ -710,6 +715,12 @@ class Display : public PollingComponent {
DisplayRotation get_rotation() const { return this->rotation_; }
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
/// neighbours along x land a row apart in the frame buffer. Not the same as
/// get_width()/get_height() swapping: a display that rotates in hardware
/// reports swapped dimensions but writes unswapped pixels.
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
@@ -758,6 +769,13 @@ class Display : public PollingComponent {
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
/// Whether (x, y) falls outside the active clipping rectangle. Tests the
/// stack top in place: get_clipping() is out of line and returns the Rect
/// by value, which per pixel drawing cannot afford.
bool ESPHOME_ALWAYS_INLINE is_point_clipped(int x, int y) const {
return this->is_clipping() && !this->clipping_rectangle_.back().inside(x, y);
}
/** Check if pixel is within region of display.
*/
bool clip(int x, int y);
@@ -774,6 +792,17 @@ class Display : public PollingComponent {
void do_update_();
void clear_clipping_();
/// Watchdog feed for per pixel loops. App.feed_wdt() is already rate
/// limited, but every call reads the clock; only every 256th pixel makes
/// that call, so the real feeds are unchanged and a pixel costs a counter.
/// At 20 us per pixel on the slowest e-paper path that is about 5 ms
/// between clock reads.
void ESPHOME_ALWAYS_INLINE feed_wdt_per_pixel_() {
if (++this->wdt_pixel_counter_ == 0)
this->feed_wdt_pixel_slow_();
}
void feed_wdt_pixel_slow_();
virtual int get_height_internal() = 0;
virtual int get_width_internal() = 0;
@@ -793,6 +822,7 @@ class Display : public PollingComponent {
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
bool auto_clear_enabled_{true};
std::vector<Rect> clipping_rectangle_;
uint8_t wdt_pixel_counter_{0};
bool show_test_card_{false};
};
@@ -2,7 +2,6 @@
#include <utility>
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::display {
@@ -44,7 +43,7 @@ int DisplayBuffer::get_height() {
}
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
if (this->is_point_clipped(x, y))
return; // NOLINT
switch (this->rotation_) {
@@ -64,7 +63,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
break;
}
this->draw_absolute_pixel_internal(x, y, color);
App.feed_wdt();
this->feed_wdt_per_pixel_();
}
} // namespace esphome::display
@@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/color.h"
#include "esphome/core/helpers.h"
namespace esphome::display {
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
@@ -8,6 +9,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
class ColorUtil {
public:
/// Reads one packed pixel of the given bitness from a source buffer into the
/// value to_color() takes. BITNESS is a template argument so the byte layout
/// folds; pass it on to to_color() as a constant too, and its channel scaling
/// divides by a constant, which the compiler turns into a multiply and shift.
template<ColorBitness BITNESS>
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
if constexpr (BITNESS == COLOR_BITNESS_565) {
const size_t idx = index * 2;
if (big_endian)
return (ptr[idx] << 8) + ptr[idx + 1];
return ptr[idx] + (ptr[idx + 1] << 8);
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
const size_t idx = index * 3;
if (big_endian)
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
} else {
return ptr[index];
}
}
static Color to_color(uint32_t colorcode, ColorOrder color_order,
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
uint8_t first_color, second_color, third_color;
-10
View File
@@ -63,16 +63,6 @@ bool Rect::equal(Rect rect) const {
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
}
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOLINT
if (!this->is_set()) {
return true;
}
if (absolute) {
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
}
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
}
bool Rect::inside(Rect rect) const {
if (!this->is_set() || !rect.is_set()) {
return true;
+9 -1
View File
@@ -26,7 +26,15 @@ class Rect {
void shrink(Rect rect);
bool inside(Rect rect) const;
bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const;
bool ESPHOME_ALWAYS_INLINE inside(int16_t test_x, int16_t test_y, bool absolute = true) const {
if (!this->is_set()) {
return true;
}
if (absolute) {
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
}
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
}
bool equal(Rect rect) const;
void info(const std::string &prefix = "rect info:");
};
+1 -1
View File
@@ -299,7 +299,7 @@ bool EPaperBase::initialise(bool partial) {
* @return false if the coordinates are out of bounds
*/
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
if (!this->get_clipping().inside(x, y))
if (this->is_point_clipped(x, y))
return false;
if (this->effective_transform_ & SWAP_XY)
std::swap(x, y);
@@ -113,6 +113,7 @@ class EPaperBase : public Display,
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
void draw_pixel_at(int x, int y, Color color) override;
protected:
+58
View File
@@ -111,6 +111,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_FLASH_CHIP = "flash_chip"
CONF_KEY_ID = "key_id"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_NVS_ENCRYPTION = "nvs_encryption"
@@ -464,6 +465,20 @@ ESP32_CHIP_REVISIONS = {
"3.1": "CONFIG_ESP32_REV_MIN_3_1",
}
# Flash vendor drivers ESP-IDF can link; each costs IRAM plus a 124 B table in DRAM
# and only the one matching the flash ID is ever used
ESP32_FLASH_CHIPS = {
"gd": "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP",
"issi": "CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP",
"mxic": "CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP",
"winbond": "CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP",
"boya": "CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP",
"th": "CONFIG_SPI_FLASH_SUPPORT_TH_CHIP",
"mxic_opi": "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
}
FLASH_CHIP_GENERIC = "generic"
FLASH_CHIP_OPI = "mxic_opi" # the octal driver, ESP32-S3 only
# Socket limit configuration for ESP-IDF
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
@@ -1519,6 +1534,13 @@ def final_validate(config) -> None:
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION],
)
)
if config[CONF_VARIANT] != VARIANT_ESP32S3 and config.get(CONF_FLASH_MODE) == "opi":
errs.append(
cv.Invalid(
f"'{CONF_FLASH_MODE}: opi' is only supported on {VARIANT_ESP32S3}",
path=[CONF_FLASH_MODE],
)
)
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]:
errs.append(
cv.Invalid(
@@ -1526,6 +1548,25 @@ def final_validate(config) -> None:
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
)
)
if (flash_chip := advanced.get(CONF_FLASH_CHIP)) is not None:
opi = flash_chip == FLASH_CHIP_OPI
if opi and config[CONF_VARIANT] != VARIANT_ESP32S3:
errs.append(
cv.Invalid(
f"'{CONF_FLASH_CHIP}: {flash_chip}' is only supported on {VARIANT_ESP32S3}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
)
)
elif opi != (config.get(CONF_FLASH_MODE) == "opi"):
errs.append(
cv.Invalid(
f"'{CONF_FLASH_CHIP}: {flash_chip}' requires '{CONF_FLASH_MODE}: opi'"
if opi
else f"'{CONF_FLASH_CHIP}: {flash_chip}' does not match "
f"'{CONF_FLASH_MODE}: opi'; octal flash uses {FLASH_CHIP_OPI}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
)
)
if (
config[CONF_VARIANT] != VARIANT_ESP32P4
and config.get(CONF_ENGINEERING_SAMPLE) is not None
@@ -1964,6 +2005,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
*ESP32_CHIP_REVISIONS, string=True
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
cv.Optional(CONF_FLASH_CHIP): cv.one_of(
FLASH_CHIP_GENERIC, *ESP32_FLASH_CHIPS, lower=True
),
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
# it will handle disabling DHCP server when AP is not configured.
# Default to false (disabled) when WiFi is not used.
@@ -2609,6 +2653,13 @@ async def to_code(config):
# NVS finds stored preferences by key, so preference key migration is possible
cg.add_define("USE_PREFERENCE_KEY_LOOKUP")
cg.add_build_flag("-Wl,-z,noexecstack")
# assert(), HAL_ASSERT and ESP_ERROR_CHECK bake __FILE__ into rodata, and
# IDF's noflash placement puts the flash driver's copies in DRAM. The
# basename keeps the panic output useful at a fraction of the size.
# __FILE_NAME__ is a GCC 12 builtin; IDF 5.0 still ships GCC 11.2.
if idf_version() >= cv.Version(5, 1, 0):
cg.add_build_flag("-D__FILE__=__FILE_NAME__")
cg.add_build_flag("-Wno-builtin-macro-redefined")
# Deferred so KEY_COMPONENTS is fully populated -- see the coroutine.
CORE.add_job(_finalize_arduino_aware_flags)
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
@@ -2725,6 +2776,8 @@ async def to_code(config):
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True
)
# the opi mode choice only exists once octal flash is enabled
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_OCT_FLASH", flash_mode == "opi")
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
@@ -2749,6 +2802,11 @@ async def to_code(config):
add_idf_sdkconfig_option(flag, rev == min_rev)
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
# Keep only the flash vendor driver the board needs; the boot log names it
if (flash_chip := conf[CONF_ADVANCED].get(CONF_FLASH_CHIP)) is not None:
for chip, flag in ESP32_FLASH_CHIPS.items():
add_idf_sdkconfig_option(flag, chip == flash_chip)
# Use SRAM1 region as IRAM on ESP32 (original) variant
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
+9 -5
View File
@@ -173,7 +173,10 @@ static const char *const TAG = "esp32.crash";
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME);
void crash_handler_read_and_clear() {
// Validate the NOINIT record. Runs on every has_data() call; re-running is
// harmless and the magic is left alone so the record survives an OTA
// rollback reboot, crash_handler_clear() drops it once an API client has it.
static void read_crash_data() {
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
s_crash_data_valid = true;
// Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
@@ -194,11 +197,12 @@ void crash_handler_read_and_clear() {
s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
#endif
}
// Don't clear magic here — crash data must survive OTA rollback reboots.
// Magic is cleared by crash_handler_clear() after an API client receives the data.
}
bool crash_handler_has_data() { return s_crash_data_valid; }
bool crash_handler_has_data() {
read_crash_data();
return s_crash_data_valid;
}
void crash_handler_clear() {
// Only clear the magic so data doesn't survive the next reboot.
@@ -426,7 +430,7 @@ static void log_foreign_addresses() {
// crashes again during boot, and allowing the CLI's process_stacktrace to match
// and decode each address individually.
void crash_handler_log() {
if (!s_crash_data_valid)
if (!crash_handler_has_data())
return;
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
+1 -6
View File
@@ -4,11 +4,6 @@
namespace esphome::esp32 {
/// Read and validate crash data from NOINIT memory.
/// Does not clear the magic marker — call crash_handler_clear() after
/// the data has been delivered to an API client so it survives OTA rollback reboots.
void crash_handler_read_and_clear();
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
@@ -16,7 +11,7 @@ void crash_handler_log();
/// Call after the data has been delivered to an API client.
void crash_handler_clear();
/// Returns true if crash data was found this boot.
/// Returns true if crash data was found this boot, reading it first if needed.
bool crash_handler_has_data();
} // namespace esphome::esp32
-8
View File
@@ -1,9 +1,6 @@
#ifdef USE_ESP32
// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set
// before crash_handler.h's #ifdef-guarded namespace block is parsed.
#include "esphome/core/defines.h"
#include "crash_handler.h"
#include "esphome/core/hal.h"
#include <esp_clk_tree.h>
@@ -45,11 +42,6 @@ void arch_restart() {
}
void arch_init() {
#ifdef USE_ESP32_CRASH_HANDLER
// Read crash data from previous boot before anything else
esp32::crash_handler_read_and_clear();
#endif
// Enable the task watchdog only on the loop task (from which we're currently running)
esp_task_wdt_add(nullptr);
@@ -597,7 +597,7 @@ async def to_code(config):
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
# Only advertise for the server itself when the configuration gives clients something to
# find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays
# find. A server that is auto-loaded purely to host a runtime service (improv_ble) stays
# silent until that service asks for advertising.
cg.add(
var.set_advertising_required(
@@ -40,7 +40,7 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
/** Whether this server needs the device to advertise so clients can find and connect to it.
*
* False for a server that only hosts services created at runtime (e.g. esp32_improv), which
* False for a server that only hosts services created at runtime (e.g. improv_ble), which
* request advertising themselves for as long as they need it.
*/
void set_advertising_required(bool required) { this->advertising_required_ = required; }
@@ -135,7 +135,7 @@ class ESPBTClient : public ESPBTDeviceListener {
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
// Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially
uint8_t app_id{0}; // App IDs are small integers assigned sequentially
protected:
/// Set state without IDLE handling - use for direct state transitions.
@@ -433,25 +433,10 @@ void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) {
}
}
void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; }
void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; }
void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; }
void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; }
void ESP32Camera::set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
/* set exposure parameters */
void ESP32Camera::set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; }
void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
/* set gains parameters */
void ESP32Camera::set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
void ESP32Camera::set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
void ESP32Camera::set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
/* set white balance */
void ESP32Camera::set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
/* set test mode */
void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
/* set fps */
void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) {
this->max_update_interval_ = max_update_interval;
+15 -15
View File
@@ -140,25 +140,25 @@ class ESP32Camera final : public camera::Camera {
void set_pixel_format(ESP32CameraPixelFormat format);
void set_frame_size(ESP32CameraFrameSize size);
void set_jpeg_quality(uint8_t quality);
void set_vertical_flip(bool vertical_flip);
void set_horizontal_mirror(bool horizontal_mirror);
void set_contrast(int contrast);
void set_brightness(int brightness);
void set_saturation(int saturation);
void set_special_effect(ESP32SpecialEffect effect);
void set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
void set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
void set_contrast(int contrast) { this->contrast_ = contrast; }
void set_brightness(int brightness) { this->brightness_ = brightness; }
void set_saturation(int saturation) { this->saturation_ = saturation; }
void set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
/* -- exposure */
void set_aec_mode(ESP32GainControlMode mode);
void set_aec2(bool aec2);
void set_ae_level(int ae_level);
void set_aec_value(uint32_t aec_value);
void set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
void set_aec2(bool aec2) { this->aec2_ = aec2; }
void set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
void set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
/* -- gains */
void set_agc_mode(ESP32GainControlMode mode);
void set_agc_value(uint8_t agc_value);
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling);
void set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
void set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
/* -- white balance */
void set_wb_mode(ESP32WhiteBalanceMode mode);
void set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
/* -- test */
void set_test_pattern(bool test_pattern);
void set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
/* -- framerates */
void set_max_update_interval(uint32_t max_update_interval);
void set_idle_update_interval(uint32_t idle_update_interval);
@@ -169,7 +169,7 @@ void Esp32HostedUpdate::dump_config() {
ESP_LOGCONFIG(TAG,
" Mode: HTTP\n"
" Source URL: %s",
this->source_url_.c_str());
this->source_url_);
#else
ESP_LOGCONFIG(TAG,
" Mode: Embedded\n"
@@ -215,7 +215,7 @@ bool Esp32HostedUpdate::fetch_manifest_() {
auto container = this->http_request_parent_->get(this->source_url_);
if (container == nullptr || container->status_code != 200) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_);
this->status_set_error(LOG_STR("Failed to fetch manifest"));
return false;
}
@@ -25,7 +25,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// HTTP mode setters
void set_source_url(const std::string &url) { this->source_url_ = url; }
void set_source_url(const char *url) { this->source_url_ = url; }
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
#else
// Embedded mode setters
@@ -38,7 +38,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// HTTP mode members
http_request::HttpRequestComponent *http_request_parent_{nullptr};
std::string source_url_;
const char *source_url_{nullptr}; // literal from codegen
std::string firmware_url_;
// HTTP mode helpers
-8
View File
@@ -363,14 +363,6 @@ async def to_code(config: ConfigType) -> None:
if config.get(CONF_ENABLE_SERIAL1):
enable_serial1()
# Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
# out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
# new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of
# a NULL pointer (so the stacktrace makes more sense), and for consistency with Arduino 3,
# which always aborts if exceptions are disabled.
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
cg.add_build_flag("-DNEW_OOM_ABORT")
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
# See throw_stubs.h for details. Must be prepended before <string>, so this
+4 -1
View File
@@ -11,6 +11,9 @@ namespace esphome::esp8266_pwm {
class ESP8266PWM final : public output::FloatOutput, public Component {
public:
// User provided, not "= default": `new(p) ESP8266PWM()` would zero-fill .bss that is already zero.
ESP8266PWM() {}
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void set_frequency(float frequency) { this->frequency_ = frequency; }
/// Dynamically update frequency
@@ -28,7 +31,7 @@ class ESP8266PWM final : public output::FloatOutput, public Component {
protected:
void write_state(float state) override;
InternalGPIOPin *pin_;
InternalGPIOPin *pin_{nullptr};
float frequency_{1000.0}; // Keep in sync with DEFAULT_FREQUENCY in output.py
/// Cache last output level for dynamic frequency updating
float last_output_{0.0};
+10 -2
View File
@@ -166,9 +166,17 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
CONF_PASSWORD,
)
# web_server and prometheus keep the shared listener up; the captive
# portal's copy only exists on the fallback AP and is the recovery path
# portal's copy only exists on the fallback AP and is the recovery path.
# web_server `ota: false` gates /update behind the captive portal on
# every listener
web_server_conf = full_conf.get(CONF_WEB_SERVER)
plaintext_update_reachable = (
web_server_conf.get(CONF_OTA) is not False
if web_server_conf is not None
else "prometheus" in full_conf
)
if (
(CONF_WEB_SERVER in full_conf or "prometheus" in full_conf)
plaintext_update_reachable
and any(conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf)
and any(
CONF_ENCRYPTION in conf
@@ -13,6 +13,9 @@ class IPAddressEthernetInfo final : public Component,
public text_sensor::TextSensor,
public ethernet::EthernetIPStateListener {
public:
// User provided, not "= default": `new(p) IPAddressEthernetInfo()` would zero-fill .bss that is already zero.
IPAddressEthernetInfo() {}
void setup() override;
void dump_config() override;
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
+1 -1
View File
@@ -183,7 +183,7 @@ class Fan : public EntityBase {
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
FanRestoreMode restore_mode_;
FanRestoreMode restore_mode_{FanRestoreMode::NO_RESTORE};
private:
/// Lazy-allocate preset modes vector (never freed — entity lives forever).
+12 -10
View File
@@ -42,7 +42,7 @@ from esphome.const import (
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.core import HexInt
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.external_files import RemoteFile
from esphome.types import ConfigType
@@ -76,16 +76,18 @@ def compute_local_image_path(value: str | ConfigType) -> Path:
return external_files.compute_local_file_path(DOMAIN, url)
def local_path(value: str | ConfigType) -> str:
value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
def local_path(value: Path | ConfigType) -> Path:
# cv.file_ has already resolved the path against the config dir.
return value[CONF_PATH] if isinstance(value, dict) else value
def download_file(url: str, path: Path) -> str:
def download_file(url: str, path: Path) -> Path:
# The shared NETWORK_TIMEOUT applies; a per-caller timeout would be
# silently ignored on a per-run memo hit anyway (memos key by path).
external_files.download_content(url, path)
return str(path)
# Keep the Path: config-hash normalizes Path values under the data dir,
# which a str would dump verbatim and break the CLI/add-on comparison.
return path
def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]:
@@ -93,13 +95,13 @@ def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]:
return MDI_SOURCES[source] + mdi_id + ".svg", base_dir / f"{mdi_id}.svg"
def download_gh_svg(value: str | ConfigType, source: str) -> str:
def download_gh_svg(value: str | ConfigType, source: str) -> Path:
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
url, path = _gh_svg_url_path(mdi_id, source)
return download_file(url, path)
def download_image(value: str | ConfigType) -> str:
def download_image(value: str | ConfigType) -> Path:
value = value[CONF_URL] if isinstance(value, dict) else value
return download_file(value, compute_local_image_path(value))
@@ -147,7 +149,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None:
PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref)
def validate_file_shorthand(value: Any) -> str:
def validate_file_shorthand(value: Any) -> Path:
value = cv.string_strict(value)
if (remote := _parse_remote_shorthand(value)) is not None:
return download_file(remote.url, remote.path)
@@ -165,7 +167,7 @@ LOCAL_SCHEMA = cv.All(
def mdi_schema(source: str) -> cv.All:
def validate_mdi(value: ConfigType) -> str:
def validate_mdi(value: ConfigType) -> Path:
return download_gh_svg(value, source)
return cv.All(
@@ -47,6 +47,9 @@ class GPIOBinarySensorStore {
class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component {
public:
// User provided, not "= default": `new(p) GPIOBinarySensor()` would zero-fill .bss that is already zero.
GPIOBinarySensor() {}
// No destructor needed: ESPHome components are created at boot and live forever.
// Interrupts are only detached on reboot when memory is cleared anyway.
@@ -70,7 +73,7 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon
void loop() override;
protected:
GPIOPin *pin_;
GPIOPin *pin_{nullptr};
GPIOBinarySensorStore store_;
};
+2 -10
View File
@@ -13,18 +13,10 @@ void GPIOSwitch::setup() {
bool initial_state = this->get_initial_state_with_restore_mode().value_or(false);
// write state before setup
if (initial_state) {
this->turn_on();
} else {
this->turn_off();
}
this->control(initial_state);
this->pin_->setup();
// write after setup again for other IOs
if (initial_state) {
this->turn_on();
} else {
this->turn_off();
}
this->control(initial_state);
}
void GPIOSwitch::dump_config() {
LOG_SWITCH("", "GPIO Switch", this);
+4 -1
View File
@@ -9,6 +9,9 @@ namespace esphome::gpio {
class GPIOSwitch final : public switch_::Switch, public Component {
public:
// User provided, not "= default": `new(p) GPIOSwitch()` would zero-fill .bss that is already zero.
GPIOSwitch() {}
void set_pin(GPIOPin *pin) { pin_ = pin; }
// ========== INTERNAL METHODS ==========
@@ -25,7 +28,7 @@ class GPIOSwitch final : public switch_::Switch, public Component {
protected:
void write_state(bool state) override;
GPIOPin *pin_;
GPIOPin *pin_{nullptr};
#ifdef USE_GPIO_SWITCH_INTERLOCK
FixedVector<Switch *> interlock_;
uint32_t interlock_wait_time_{0};
@@ -57,10 +57,6 @@ void GraphicalDisplayMenu::dump_config() {
}
}
void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; }
void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; }
void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; }
void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; }
@@ -38,8 +38,8 @@ class GraphicalDisplayMenu final : public display_menu_base::DisplayMenuComponen
void setup() override;
void dump_config() override;
void set_display(display::Display *display);
void set_font(display::BaseFont *font);
void set_display(display::Display *display) { this->display_ = display; }
void set_font(display::BaseFont *font) { this->font_ = font; }
template<typename V> void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; }
void set_foreground_color(Color foreground_color);
void set_background_color(Color background_color);
-2
View File
@@ -190,8 +190,6 @@ void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets)
this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);
}
void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) {
this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message});
}
+1 -1
View File
@@ -71,7 +71,7 @@ class HaierClimateBase : public esphome::Component,
};
bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
void set_answer_timeout(uint32_t timeout);
void set_send_wifi(bool send_wifi);
void set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
void send_custom_command(const haier_protocol::HaierMessage &message);
template<typename F> void add_status_message_callback(F &&callback) {
this->status_message_callback_.add(std::forward<F>(callback));
+47 -17
View File
@@ -331,27 +331,46 @@ class HttpRequestComponent : public Component {
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
std::shared_ptr<HttpContainer> get(const std::string &url) {
return this->start(url, "GET", "", std::vector<Header>{});
}
std::shared_ptr<HttpContainer> get(const std::string &url, const std::vector<Header> &request_headers) {
std::shared_ptr<HttpContainer> get(const char *url) { return this->start(url, "GET", "", std::vector<Header>{}); }
std::shared_ptr<HttpContainer> get(const char *url, const std::vector<Header> &request_headers) {
return this->start(url, "GET", "", request_headers);
}
std::shared_ptr<HttpContainer> get(const std::string &url, const std::vector<Header> &request_headers,
std::shared_ptr<HttpContainer> get(const char *url, const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url, "GET", "", request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body) {
std::shared_ptr<HttpContainer> post(const char *url, const std::string &body) {
return this->start(url, "POST", body, std::vector<Header>{});
}
std::shared_ptr<HttpContainer> post(const char *url, const std::string &body,
const std::vector<Header> &request_headers) {
return this->start(url, "POST", body, request_headers);
}
std::shared_ptr<HttpContainer> post(const char *url, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url, "POST", body, request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> get(const std::string &url) { return this->get(url.c_str()); }
std::shared_ptr<HttpContainer> get(const std::string &url, const std::vector<Header> &request_headers) {
return this->get(url.c_str(), request_headers);
}
std::shared_ptr<HttpContainer> get(const std::string &url, const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->get(url.c_str(), request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body) {
return this->post(url.c_str(), body);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
const std::vector<Header> &request_headers) {
return this->start(url, "POST", body, request_headers);
return this->post(url.c_str(), body, request_headers);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url, "POST", body, request_headers, lower_case_collect_headers);
return this->post(url.c_str(), body, request_headers, lower_case_collect_headers);
}
// Remove before 2027.1.0
@@ -379,11 +398,15 @@ class HttpRequestComponent : public Component {
return this->post(url, body, std::vector<Header>(request_headers.begin(), request_headers.end()), collect_headers);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> start(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers) {
// Call perform() directly to avoid ambiguity with the deprecated overloads
return this->perform(url, method, body, request_headers, {});
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::vector<Header> &request_headers) {
return this->start(url.c_str(), method.c_str(), body, request_headers);
}
// Remove before 2027.1.0
ESPDEPRECATED("Pass request_headers as std::vector<Header> instead of std::list. Removed in 2027.1.0.", "2026.7.0")
@@ -403,7 +426,7 @@ class HttpRequestComponent : public Component {
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
}
return this->perform(url, method, body, request_headers, lower);
return this->perform(url.c_str(), method.c_str(), body, request_headers, lower);
}
// Remove before 2027.1.0
@@ -418,7 +441,8 @@ class HttpRequestComponent : public Component {
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
}
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
return this->perform(url.c_str(), method.c_str(), body,
std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
}
// Remove before 2027.1.0
@@ -426,19 +450,25 @@ class HttpRequestComponent : public Component {
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()),
return this->perform(url.c_str(), method.c_str(), body,
std::vector<Header>(request_headers.begin(), request_headers.end()),
lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> start(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->perform(url, method, body, request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url.c_str(), method.c_str(), body, request_headers, lower_case_collect_headers);
}
protected:
virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
const std::string &body, const std::vector<Header> &request_headers,
virtual std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) = 0;
const char *useragent_{nullptr};
bool follow_redirects_{};
@@ -499,8 +529,8 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
request_headers.push_back({key, val.value(x...)});
}
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
this->lower_case_collect_headers_);
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
request_headers, this->lower_case_collect_headers_);
auto captured_args = std::make_tuple(x...);
@@ -2,6 +2,8 @@
#if defined(USE_ARDUINO) && !defined(USE_ESP32) && !defined(USE_LIBRETINY)
#include <cstring>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"
@@ -22,8 +24,7 @@ static const char *const TAG = "http_request";
static constexpr int ESP8266_SSL_ERR_OOM = -1000;
#endif
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
const std::string &body,
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
@@ -37,7 +38,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
const uint32_t start = millis();
bool secure = url.find("https:") != std::string::npos;
bool secure = strstr(url, "https:") != nullptr;
container->set_secure(secure);
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
@@ -70,19 +71,19 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
stream_ptr = std::make_unique<WiFiClient>();
#endif // USE_HTTP_REQUEST_ESP8266_HTTPS
bool status = container->client_.begin(*stream_ptr, url.c_str());
bool status = container->client_.begin(*stream_ptr, url);
#elif defined(USE_RP2)
if (secure) {
container->client_.setInsecure();
}
bool status = container->client_.begin(url.c_str());
bool status = container->client_.begin(url);
#endif
App.feed_wdt();
if (!status) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url.c_str());
ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url);
container->end();
this->status_momentary_error("failed", 1000);
return nullptr;
@@ -107,7 +108,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
container->client_.collectHeaders(header_keys, index);
App.feed_wdt();
container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
container->status_code = container->client_.sendRequest(method, body.c_str());
App.feed_wdt();
if (container->status_code < 0) {
#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS)
@@ -139,7 +140,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
}
#endif
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url,
HTTPClient::errorToString(container->status_code).c_str());
this->status_momentary_error("failed", 1000);
@@ -147,7 +148,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
return nullptr;
}
if (!is_success(container->status_code)) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
this->status_momentary_error("failed", 1000);
// Still return the container, so it can be used to get the status code and error message
}
@@ -54,7 +54,7 @@ class HttpRequestArduino final : public HttpRequestComponent {
#endif
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) override;
#ifdef USE_ESP8266
@@ -5,6 +5,8 @@
#include "httplib.h"
#include "http_request_host.h"
#include <cstring>
#include <regex>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"
@@ -16,8 +18,7 @@ namespace esphome::http_request {
static const char *const TAG = "http_request";
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
const std::string &body,
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
@@ -27,10 +28,10 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
}
std::regex url_regex(R"(^(([^:\/?#]+):)?(//([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)", std::regex::extended);
std::smatch url_match_result;
std::cmatch url_match_result;
if (!std::regex_match(url, url_match_result, url_regex) || url_match_result.length() < 7) {
ESP_LOGE(TAG, "HTTP Request failed; Malformed URL: %s", url.c_str());
ESP_LOGE(TAG, "HTTP Request failed; Malformed URL: %s", url);
return nullptr;
}
auto host = url_match_result[4].str();
@@ -54,7 +55,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
}
httplib::Client client(scheme_host.c_str());
if (!client.is_valid()) {
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url.c_str());
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url);
return nullptr;
}
client.set_follow_location(this->follow_redirects_);
@@ -64,41 +65,41 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
#endif
httplib::Result result;
if (method == "GET") {
if (strcmp(method, "GET") == 0) {
result = client.Get(path, h_headers, [&](const char *data, size_t data_length) {
ESP_LOGV(TAG, "Got data length: %zu", data_length);
container->response_body_.insert(container->response_body_.end(), (const uint8_t *) data,
(const uint8_t *) data + data_length);
return true;
});
} else if (method == "HEAD") {
} else if (strcmp(method, "HEAD") == 0) {
result = client.Head(path, h_headers);
} else if (method == "PUT") {
} else if (strcmp(method, "PUT") == 0) {
result = client.Put(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else if (method == "PATCH") {
} else if (strcmp(method, "PATCH") == 0) {
result = client.Patch(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else if (method == "POST") {
} else if (strcmp(method, "POST") == 0) {
result = client.Post(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else {
ESP_LOGW(TAG, "HTTP Request failed - unsupported method %s; URL: %s", method.c_str(), url.c_str());
ESP_LOGW(TAG, "HTTP Request failed - unsupported method %s; URL: %s", method, url);
container->end();
return nullptr;
}
App.feed_wdt();
if (!result) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url.c_str(), (unsigned) result.error());
ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url, (unsigned) result.error());
container->end();
this->status_momentary_error("failed", 1000);
return nullptr;
@@ -107,7 +108,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
auto response = *result;
container->status_code = response.status;
if (!is_success(response.status)) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), response.status);
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, response.status);
this->status_momentary_error("failed", 1000);
// Still return the container, so it can be used to get the status code and error message
}
@@ -18,7 +18,7 @@ class HttpContainerHost : public HttpContainer {
class HttpRequestHost final : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) override;
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
@@ -2,6 +2,8 @@
#ifdef USE_ESP32
#include <cstring>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"
@@ -48,8 +50,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
return ESP_OK;
}
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
const std::string &body,
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
@@ -59,15 +60,15 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
esp_http_client_method_t method_idf;
if (method == "GET") {
if (strcmp(method, "GET") == 0) {
method_idf = HTTP_METHOD_GET;
} else if (method == "POST") {
} else if (strcmp(method, "POST") == 0) {
method_idf = HTTP_METHOD_POST;
} else if (method == "PUT") {
} else if (strcmp(method, "PUT") == 0) {
method_idf = HTTP_METHOD_PUT;
} else if (method == "DELETE") {
} else if (strcmp(method, "DELETE") == 0) {
method_idf = HTTP_METHOD_DELETE;
} else if (method == "PATCH") {
} else if (strcmp(method, "PATCH") == 0) {
method_idf = HTTP_METHOD_PATCH;
} else {
this->status_momentary_error("failed", ERROR_DURATION_MS);
@@ -75,11 +76,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return nullptr;
}
bool secure = url.find("https:") != std::string::npos;
bool secure = strstr(url, "https:") != nullptr;
esp_http_client_config_t config = {};
config.url = url.c_str();
config.url = url;
config.method = method_idf;
config.timeout_ms = this->timeout_;
config.disable_auto_redirect = !this->follow_redirects_;
@@ -218,7 +219,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
}
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
this->status_momentary_error("failed", ERROR_DURATION_MS);
return container;
}
@@ -30,6 +30,9 @@ class HttpContainerIDF : public HttpContainer {
class HttpRequestIDF final : public HttpRequestComponent {
public:
// User provided, not "= default": `new(p) HttpRequestIDF()` would zero-fill .bss that is already zero.
HttpRequestIDF() {}
void dump_config() override;
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
@@ -38,7 +41,7 @@ class HttpRequestIDF final : public HttpRequestComponent {
void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; }
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) override;
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
@@ -1,5 +1,7 @@
#include "http_request_update.h"
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/version.h"
@@ -94,7 +96,7 @@ void HttpRequestUpdate::update_task(void *params) {
auto container = this_update->request_parent_->get(this_update->source_url_);
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_);
if (container != nullptr)
container->end();
result->error_str = LOG_STR("Failed to fetch manifest");
@@ -174,21 +176,26 @@ void HttpRequestUpdate::update_task(void *params) {
allocator.deallocate(data, content_length);
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_);
result->error_str = LOG_STR("Failed to parse manifest JSON");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Merge source_url_ and firmware_url
if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
std::string path = info->firmware_url;
if (path[0] == '/') {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
info->firmware_url = domain + path;
const char *source = this_update->source_url_;
const size_t source_len = strlen(source);
size_t prefix_len;
if (info->firmware_url[0] == '/') {
// scheme and host, up to the first slash after "https://"
const char *host_end = source_len > 8 ? strchr(source + 8, '/') : nullptr;
prefix_len = host_end != nullptr ? host_end - source : source_len;
} else {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
info->firmware_url = domain + path;
// directory of the manifest, up to and including its last slash
const char *dir_end = strrchr(source, '/');
prefix_len = dir_end != nullptr ? dir_end - source + 1 : 0;
}
info->firmware_url.insert(0, source, prefix_len);
}
#ifdef ESPHOME_PROJECT_VERSION
@@ -21,7 +21,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo
void perform(bool force) override;
void check() override { this->update(); }
void set_source_url(const std::string &source_url) { this->source_url_ = source_url; }
void set_source_url(const char *source_url) { this->source_url_ = source_url; }
void set_request_parent(HttpRequestComponent *request_parent) { this->request_parent_ = request_parent; }
void set_ota_parent(OtaHttpRequestComponent *ota_parent) { this->ota_parent_ = ota_parent; }
@@ -33,13 +33,15 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo
protected:
HttpRequestComponent *request_parent_;
OtaHttpRequestComponent *ota_parent_;
std::string source_url_;
static void update_task(void *params);
#ifdef USE_ESP32
TaskHandle_t update_task_handle_{nullptr};
#endif
uint8_t initial_check_remaining_{0};
private:
const char *source_url_{nullptr}; // literal from codegen
};
} // namespace esphome::http_request
+2 -3
View File
@@ -1,5 +1,4 @@
#include "hub75_component.h"
#include "esphome/core/application.h"
#include <cinttypes>
@@ -124,11 +123,11 @@ void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]]
return;
if (!this->get_clipping().inside(x, y))
if (this->is_point_clipped(x, y))
return;
driver_->set_pixel(x, y, color.r, color.g, color.b);
App.feed_wdt();
this->feed_wdt_per_pixel_();
}
void HOT HUB75Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
@@ -48,10 +48,11 @@ static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, Tic
auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx);
size_t bytes_written = 0;
esp_err_t err = i2s_channel_write(speaker->get_tx_handle(), data, size, &bytes_written, ticks_to_wait);
if (err != ESP_OK) {
if (err != ESP_OK || bytes_written != size) {
ESP_LOGV(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size);
return (err != ESP_OK) ? err : ESP_FAIL;
}
return err;
return ESP_OK;
}
void I2SAudioSpeakerSPDIF::setup() {
@@ -167,33 +168,44 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
}
}
if (!successful_setup) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
} else {
// Preload DMA buffers with SPDIF-encoded silence before enabling the channel.
// This ensures the first data transmitted is valid SPDIF (not raw zeros from
// auto_clear) and prevents phantom DMA events before real audio is available.
// Each preloaded block pushes a 0-real-frame record so that the corresponding
// on_sent events drain in lockstep without crediting any audio frames.
// Preload DMA buffers with SPDIF-encoded silence before enabling the channel.
// This ensures the first data transmitted is valid SPDIF (not raw zeros from
// auto_clear) and prevents phantom DMA events before real audio is available.
// Each preloaded block pushes a 0-real-frame record so that the corresponding
// on_sent events drain in lockstep without crediting any audio frames. Runs with
// the channel disabled: at startup and after a resync.
auto preload_silence = [&]() -> bool {
bool ok = true;
this->spdif_encoder_->set_preload_mode(true);
for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) {
// i2s_channel_preload_data is non-blocking (returns immediately when the preload buffer fills), so no wait.
esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(0);
if (preload_err != ESP_OK) {
break; // DMA preload buffer full or error
}
const uint32_t silence_record = 0;
xQueueSendToBack(this->write_records_queue_, &silence_record, 0);
if ((this->spdif_encoder_->flush_with_silence(0) != ESP_OK) ||
(xQueueSendToBack(this->write_records_queue_, &silence_record, 0) != pdTRUE)) {
ok = false;
break;
}
}
this->spdif_encoder_->set_preload_mode(false);
this->spdif_encoder_->reset(); // Clean encoder state for the main loop
return ok;
};
// Now register the callback and enable the channel
if (successful_setup) {
successful_setup = preload_silence();
}
if (successful_setup) {
// Register the callback before enabling so the first transmitted block generates a queued event.
xQueueReset(this->i2s_event_queue_);
const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb};
i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
i2s_channel_enable(this->tx_handle_);
successful_setup = i2s_channel_enable(this->tx_handle_) == ESP_OK;
}
if (!successful_setup) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
} else {
// Always-fill model: each iteration produces exactly one SPDIF block (= one DMA buffer).
// We drain real PCM up to one block from the ring buffer and silence-pad any remainder.
// Blocking writes pace the loop at the DMA consumption rate. This mirrors the standard
@@ -210,24 +222,20 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
uint32_t spdif_pending_frames = 0;
int64_t spdif_pending_timestamp = 0;
uint32_t spdif_dma_event_count = 0;
bool resync_needed = false;
// Real frames consumed from the ring buffer that never reached a write record
uint32_t unrecorded_frames = 0;
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
// SPDIF continuous mode: loop runs indefinitely, outputting silence when no audio data
// to keep the receiver synced. Exits only via break (stream info change, silence timeout,
// lockstep desync, dropped event, or partial-write failure).
// or a failed lockstep resync).
while (true) {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
// The ISR pairs COMMAND_STOP with ERR_DROPPED_EVENT when it has to discard a completion
// event; that desyncs the lockstep queues permanently and the only safe recovery is a full
// task restart.
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
ESP_LOGV(TAG, "Exiting: ISR dropped event, restarting to recover lockstep");
break;
}
// User-initiated stop. In SPDIF continuous mode, transition to silence output rather
// than tearing the task down.
this->spdif_silence_start_ = millis();
@@ -244,6 +252,30 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
break;
}
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
ESP_LOGE(TAG, "ISR event queue overflow, resyncing DMA lockstep");
resync_needed = true;
}
if (resync_needed) {
// Rebuild the lockstep in place. Frames held back by decimation are credited too, since their
// blocks are discarded with the rest of the DMA contents.
this->spdif_encoder_->reset();
const uint32_t credited_frames = unrecorded_frames + spdif_pending_frames;
const bool resynced = this->resync_lockstep_(credited_frames, preload_silence);
unrecorded_frames = 0;
spdif_pending_frames = 0;
spdif_dma_event_count = 0;
resync_needed = false;
if (credited_frames > 0) {
// Real audio was dropped, so the silence timer's start no longer reflects the stream
this->spdif_silence_start_ = 0;
}
if (!resynced) {
ESP_LOGE(TAG, "DMA lockstep resync failed, restarting speaker task");
break;
}
}
// Drain ISR completion events, popping a matching record for each.
int64_t write_timestamp;
bool lockstep_broken = false;
@@ -253,8 +285,7 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
// order matches DMA completion order. Empty records queue here means lockstep broke.
uint32_t real_frames = 0;
if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) {
ESP_LOGV(TAG, "Event without matching write record");
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
ESP_LOGE(TAG, "Event without matching write record, resyncing DMA lockstep");
lockstep_broken = true;
break;
}
@@ -290,8 +321,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
}
}
if (lockstep_broken) {
ESP_LOGV(TAG, "Exiting: lockstep desync, restarting task");
break;
resync_needed = true;
continue;
}
// Always-fill: produce exactly one SPDIF block this iteration. The blocking encoder write
@@ -322,9 +353,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
&blocks_sent, &pcm_consumed);
if (err != ESP_OK) {
// A failed (or timed-out) send leaves an unsent block in the encoder's stitch buffer;
// resuming would credit the next iteration's bytes against an old block. Bail and
// let loop() restart the task with a clean encoder.
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
// resuming would credit the next iteration's bytes against an old block.
ESP_LOGE(TAG, "SPDIF block send failed, resyncing DMA lockstep");
partial_write_failure = true;
break;
}
@@ -341,7 +371,9 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
}
if (partial_write_failure) {
break;
unrecorded_frames += real_frames_in_block;
resync_needed = true;
continue;
}
if (!block_committed) {
@@ -349,16 +381,20 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
// or emit a full silence block if the encoder is empty.
esp_err_t err = this->spdif_encoder_->flush_with_silence(write_timeout_ticks);
if (err != ESP_OK) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
break;
ESP_LOGE(TAG, "SPDIF block send failed, resyncing DMA lockstep");
unrecorded_frames += real_frames_in_block;
resync_needed = true;
continue;
}
}
// One block committed to DMA; push exactly one record carrying its real-audio frame count.
// Failure here means the records queue is full, which violates the lockstep invariant.
if (xQueueSendToBack(this->write_records_queue_, &real_frames_in_block, 0) != pdTRUE) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
break;
ESP_LOGE(TAG, "Write records queue full, resyncing DMA lockstep");
unrecorded_frames += real_frames_in_block;
resync_needed = true;
continue;
}
// Silence-timeout tracking and graceful-stop reset.
@@ -14,17 +14,19 @@
#include "esp_timer.h"
// esp-audio-libs
#include <gain.h>
#include <cmath>
namespace esphome::i2s_audio {
static const char *const TAG = "i2s_audio.speaker";
// Software volume control maps the user-facing [0.0, 1.0] range to a Q31 scale factor.
// Volumes in (0.0, 1.0) map linearly to a dB reduction in [-49.0, 0.0] dB.
// Software volume control maps the user-facing (0.0, 1.0) range linearly to a dB reduction in
// [-49.0, 0.0] dB; 0.0 is silence.
static constexpr float SOFTWARE_VOLUME_MIN_DB = -49.0f;
// Rate at which the software gain moves toward a new target.
static constexpr uint32_t GAIN_RAMP_MS_PER_DB = 1;
void I2SAudioSpeakerBase::setup() {
this->event_group_ = xEventGroupCreate();
@@ -34,9 +36,10 @@ void I2SAudioSpeakerBase::setup() {
return;
}
// Initialize volume control. When audio_dac is configured, this sets the DAC volume.
// Initialize volume control. When audio_dac is configured, this sets the DAC volume and mute state.
// When no audio_dac is configured, this initializes software volume control.
this->set_volume(this->volume_);
this->set_mute_state(this->mute_state_);
}
void I2SAudioSpeakerBase::dump_config() {
@@ -77,17 +80,6 @@ void I2SAudioSpeakerBase::loop() {
}
if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
ESP_LOGV(TAG, "Stopping");
// Lockstep-breaking error bits are latched by the task and cleared along with all other bits
// when TASK_STOPPED is processed; log them here, exactly once, as the task winds down.
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
ESP_LOGE(TAG, "ISR event queue overflow, restarting speaker task to recover timestamp sync");
}
if (event_group_bits & SpeakerEventGroupBits::ERR_PARTIAL_WRITE) {
ESP_LOGE(TAG, "Partial DMA write broke buffer alignment, restarting speaker task");
}
if (event_group_bits & SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC) {
ESP_LOGE(TAG, "Event/record queues desynced, restarting speaker task");
}
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
this->state_ = speaker::STATE_STOPPING;
}
@@ -136,6 +128,10 @@ void I2SAudioSpeakerBase::loop() {
break;
}
// Seed the ramp at the live target so this run adopts it instantly rather than fading to it
// from wherever the previous run left off. Posted here, not in the task: the ramp's mailbox
// allows one writer, and that is the main loop.
this->post_software_gain_(0);
xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
@@ -153,50 +149,31 @@ void I2SAudioSpeakerBase::loop() {
}
void I2SAudioSpeakerBase::set_volume(float volume) {
this->volume_ = volume;
#ifdef USE_AUDIO_DAC
if (this->audio_dac_ != nullptr) {
if (volume > 0.0f) {
this->audio_dac_->set_mute_off();
}
this->audio_dac_->set_volume(volume);
} else
#endif // USE_AUDIO_DAC
{
// Fallback to software volume control by using a Q31 fixed point scaling factor.
// At maximum volume (1.0), set to INT32_MAX to bypass volume processing entirely
// and avoid any floating-point precision issues that could cause slight volume reduction.
if (volume >= 1.0f) {
this->q31_volume_factor_ = INT32_MAX;
} else if (volume <= 0.0f) {
this->q31_volume_factor_ = 0;
} else {
this->q31_volume_factor_ =
esp_audio_libs::gain::db_to_q31(remap<float, float>(volume, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f));
}
}
speaker::Speaker::set_volume(volume);
this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
}
void I2SAudioSpeakerBase::set_mute_state(bool mute_state) {
this->mute_state_ = mute_state;
speaker::Speaker::set_mute_state(mute_state);
this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
}
void I2SAudioSpeakerBase::post_software_gain_(uint32_t rate_samples) {
#ifdef USE_AUDIO_DAC
if (this->audio_dac_) {
if (mute_state) {
this->audio_dac_->set_mute_on();
} else {
this->audio_dac_->set_mute_off();
}
} else
#endif // USE_AUDIO_DAC
{
if (mute_state) {
// Fallback to software volume control and scale by 0
this->q31_volume_factor_ = 0;
} else {
// Revert to previous volume when unmuting
this->set_volume(this->volume_);
}
if (this->audio_dac_ != nullptr) {
return; // Hardware volume; the ramp stays at unity
}
#endif // USE_AUDIO_DAC
// Software volume control. The ramp treats 0 dB as unity and skips processing there.
float target_db;
if (this->is_silent_()) {
target_db = -INFINITY;
} else if (this->volume_ >= 1.0f) {
target_db = 0.0f;
} else {
target_db = remap<float, float>(this->volume_, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f);
}
this->gain_ramp_.set_target_db_at_rate(target_db, rate_samples);
}
size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
@@ -337,16 +314,10 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx;
if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) {
// Queue is full, so discard the oldest event. Once we drop a completion event, ``i2s_event_queue_``
// and any per-buffer record queue maintained by the task are permanently desynced, so the task
// must restart to recover. Set both ERR_DROPPED_EVENT (so loop() can log it) and COMMAND_STOP
// (so the task bails immediately, closing the race where loop() could clear the error bit
// before the task observes it).
// Queue is full, so discard the oldest event. The lockstep queues are now desynced; the task resyncs them.
int64_t dummy;
xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1);
xEventGroupSetBitsFromISR(this_speaker->event_group_,
SpeakerEventGroupBits::ERR_DROPPED_EVENT | SpeakerEventGroupBits::COMMAND_STOP,
&need_yield2);
xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT, &need_yield2);
}
xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3);
@@ -354,15 +325,33 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
return need_yield1 | need_yield2 | need_yield3;
}
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
if (this->q31_volume_factor_ == INT32_MAX) {
return; // Max volume, no processing needed
void I2SAudioSpeakerBase::drain_lockstep_(uint32_t extra_frames) {
// Stop DMA so no more completion events arrive while the queues are rebuilt
i2s_channel_disable(this->tx_handle_);
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT);
uint32_t frames = extra_frames;
uint32_t record_frames = 0;
while (xQueueReceive(this->write_records_queue_, &record_frames, 0) == pdTRUE) {
frames += record_frames;
}
xQueueReset(this->i2s_event_queue_);
if (frames > 0) {
ESP_LOGV(TAG, "Crediting %" PRIu32 " dropped frames as played", frames);
this->audio_output_callback_(frames, esp_timer_get_time());
}
}
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
#ifdef USE_AUDIO_DAC
if (this->audio_dac_ != nullptr) {
return; // Hardware volume; the ramp is never targeted
}
#endif // USE_AUDIO_DAC
const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1);
const uint32_t len = bytes_read / bytes_per_sample;
esp_audio_libs::gain::apply(data, data, this->q31_volume_factor_, len, bytes_per_sample);
this->gain_ramp_.process(data, static_cast<uint8_t>(bytes_per_sample),
this->current_stream_info_.bytes_to_samples(bytes_read));
}
void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
@@ -16,6 +16,8 @@
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include <gain.h> // esp-audio-libs
namespace esphome::i2s_audio {
// Shared constants used by both standard and SPDIF speaker implementations
@@ -34,9 +36,7 @@ enum SpeakerEventGroupBits : uint32_t {
ERR_ESP_NO_MEM = (1 << 19),
ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event
ERR_PARTIAL_WRITE = (1 << 21), // i2s_channel_write returned fewer bytes than requested
ERR_LOCKSTEP_DESYNC = (1 << 22), // i2s_event_queue_ and write_records_queue_ fell out of sync
ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
@@ -77,19 +77,23 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
bool has_buffered_data() const override;
/// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
/// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
/// Q15 fixed-point factor.
/// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailable, it is
/// implemented as a software volume control. Overrides the default setter to convert the volume to a dB target for
/// the gain ramp.
/// @param volume between 0.0 and 1.0
void set_volume(float volume) override;
/// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
/// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
/// Q15 fixed-point factor.
/// @brief Mutes or unmutes the speaker. Uses the speaker's configured audio dac component. If unavailable, it is
/// implemented as a software volume control. Overrides the default setter to post the mute state to the gain ramp.
/// @param mute_state true for muting, false for unmuting
void set_mute_state(bool mute_state) override;
protected:
/// @brief Posts the ramp target derived from the current volume and mute state. No-op when an audio dac owns
/// volume. Main loop only.
/// @param rate_samples Samples the ramp takes per dB of change; 0 adopts the target at once
void post_software_gain_(uint32_t rate_samples);
/// @brief FreeRTOS task entry point. Casts params to I2SAudioSpeakerBase and calls run_speaker_task_().
/// @param params I2SAudioSpeakerBase component pointer
static void speaker_task(void *params);
@@ -128,7 +132,23 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
/// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup.
virtual void on_task_stopped() {}
/// @brief Apply software volume control using Q15 fixed-point scaling.
/// @brief Rebuilds the lockstep queues in place: disables the channel, credits every in-flight real frame as
/// played now, empties both queues, preloads silence through ``preload`` and re-enables the channel. Speaker
/// task only.
/// @param extra_frames Real frames the caller consumed that never reached a write record
/// @param preload Callable returning true once every DMA descriptor holds silence with a matching record
/// @return false if the preload or the channel enable failed; the caller should restart the task
template<typename F> bool resync_lockstep_(uint32_t extra_frames, F &&preload) {
this->drain_lockstep_(extra_frames);
return preload() && (i2s_channel_enable(this->tx_handle_) == ESP_OK);
}
/// @brief Disables the channel, credits ``extra_frames`` plus every real frame still recorded as in flight,
/// and empties both lockstep queues.
void drain_lockstep_(uint32_t extra_frames);
/// @brief Apply software volume control by running the samples through the gain ramp. Called from the
/// speaker task only.
/// @param data Pointer to audio sample data (modified in place)
/// @param bytes_read Number of bytes of audio data
void apply_software_volume_(uint8_t *data, size_t bytes_read);
@@ -155,7 +175,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
bool pause_state_{false};
int32_t q31_volume_factor_{INT32_MAX};
// Smooths software gain changes. The main loop posts targets, the speaker task processes;
// GainRamp's mailbox makes that safe. The main loop is the only poster.
esp_audio_libs::gain::GainRamp gain_ramp_;
audio::AudioStreamInfo current_stream_info_; // Format of the audio in the ring buffer (the I2S input)
// Format actually clocked out of the I2S peripheral. Same channel count and sample rate as
@@ -134,27 +134,29 @@ void I2SAudioSpeaker::run_speaker_task() {
}
}
if (successful_setup) {
// Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer.
// This guarantees that every on_sent event has a corresponding write record from the start, so
// ``i2s_event_queue_`` and ``write_records_queue_`` stay in lockstep for the entire task lifetime.
// Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer, so every
// on_sent event has a write record from the start. Runs with the channel disabled: at startup and after a resync.
auto preload_silence = [&]() -> bool {
for (size_t i = 0; i < DMA_BUFFERS_COUNT; i++) {
size_t bytes_loaded = 0;
esp_err_t err = i2s_channel_preload_data(this->tx_handle_, silence_buffer, dma_buffer_bytes, &bytes_loaded);
if (err != ESP_OK || bytes_loaded != dma_buffer_bytes) {
ESP_LOGV(TAG, "Failed to preload silence into DMA buffer %u (err=%d, loaded=%u)", (unsigned) i, (int) err,
(unsigned) bytes_loaded);
successful_setup = false;
break;
return false;
}
uint32_t zero_real_frames = 0;
if (xQueueSend(this->write_records_queue_, &zero_real_frames, 0) != pdTRUE) {
// Should never happen: the queue was just reset and is sized for DMA_BUFFERS_COUNT * 2 entries.
ESP_LOGV(TAG, "Failed to push preload write record");
successful_setup = false;
break;
return false;
}
}
return true;
};
if (successful_setup) {
successful_setup = preload_silence();
}
if (successful_setup) {
@@ -177,6 +179,9 @@ void I2SAudioSpeaker::run_speaker_task() {
// stop to wait until every real-audio buffer has been confirmed played by an ISR event.
uint32_t pending_real_buffers = 0;
uint32_t last_data_received_time = millis();
bool resync_needed = false;
// Real frames consumed from the ring buffer that never reached a write record
uint32_t unrecorded_frames = 0;
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
@@ -197,8 +202,6 @@ void I2SAudioSpeaker::run_speaker_task() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
// COMMAND_STOP is set both by user-initiated stop() and by the ISR when it drops a completion
// event (paired with ERR_DROPPED_EVENT so loop() can distinguish the two cases).
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
ESP_LOGV(TAG, "Exiting: COMMAND_STOP received");
break;
@@ -214,6 +217,22 @@ void I2SAudioSpeaker::run_speaker_task() {
break;
}
if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) {
ESP_LOGE(TAG, "ISR event queue overflow, resyncing DMA lockstep");
resync_needed = true;
}
if (resync_needed) {
// Rebuild the lockstep in place; the ring buffer keeps accepting audio throughout
const bool resynced = this->resync_lockstep_(unrecorded_frames, preload_silence);
unrecorded_frames = 0;
pending_real_buffers = 0;
resync_needed = false;
if (!resynced) {
ESP_LOGE(TAG, "DMA lockstep resync failed, restarting speaker task");
break;
}
}
// Drain ISR-stamped completion events. Each event corresponds 1:1 with a write_records_queue_
// entry by construction (preloaded records at startup, plus exactly one record pushed per
// iteration alongside exactly one DMA-buffer-sized write).
@@ -223,8 +242,7 @@ void I2SAudioSpeaker::run_speaker_task() {
uint32_t real_frames = 0;
if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) {
// Should never happen: would indicate the lockstep invariant is broken.
ESP_LOGV(TAG, "Event without matching write record");
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
ESP_LOGE(TAG, "Event without matching write record, resyncing DMA lockstep");
lockstep_broken = true;
break;
}
@@ -240,7 +258,8 @@ void I2SAudioSpeaker::run_speaker_task() {
}
}
if (lockstep_broken) {
break;
resync_needed = true;
continue;
}
// Graceful stop: exit only after the source's exposed chunk is drained, the underlying ring
@@ -299,10 +318,12 @@ void I2SAudioSpeaker::run_speaker_task() {
size_t bw = 0;
i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS);
if (bw != output_bytes) {
// A short real-audio write breaks DMA descriptor alignment for every subsequent event;
// the only safe recovery is to restart the task.
ESP_LOGV(TAG, "Partial real audio write: %u of %u bytes", (unsigned) bw, (unsigned) output_bytes);
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
// A short write breaks DMA descriptor alignment for every subsequent event. Drop the chunk rather
// than retry it: it was already narrowed in place.
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
(unsigned) output_bytes);
audio_source->consume(input_bytes);
real_frames_total += frames_to_write;
partial_write_failure = true;
break;
}
@@ -316,7 +337,9 @@ void I2SAudioSpeaker::run_speaker_task() {
}
if (partial_write_failure) {
break;
unrecorded_frames += real_frames_total;
resync_needed = true;
continue;
}
const size_t silence_bytes = dma_buffer_bytes - bytes_written_total;
@@ -325,19 +348,22 @@ void I2SAudioSpeaker::run_speaker_task() {
i2s_channel_write(this->tx_handle_, silence_buffer, silence_bytes, &bw, WRITE_TIMEOUT_TICKS);
if (bw != silence_bytes) {
// Same descriptor-alignment hazard as a partial real-audio write.
ESP_LOGV(TAG, "Partial silence write: %u of %u bytes", (unsigned) bw, (unsigned) silence_bytes);
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE);
break;
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
(unsigned) silence_bytes);
unrecorded_frames += real_frames_total;
resync_needed = true;
continue;
}
}
// Push the matching write record. Capacity headroom in I2S_EVENT_QUEUE_COUNT guarantees this
// succeeds even with a transient backlog of unprocessed events; if it ever fails the lockstep
// invariant is broken and every subsequent timestamp would be silently wrong, so bail.
// invariant is broken and every subsequent timestamp would be silently wrong, so rebuild it.
if (xQueueSend(this->write_records_queue_, &real_frames_total, 0) != pdTRUE) {
ESP_LOGV(TAG, "Exiting: write records queue full");
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC);
break;
ESP_LOGE(TAG, "Write records queue full, resyncing DMA lockstep");
unrecorded_frames += real_frames_total;
resync_needed = true;
continue;
}
if (real_frames_total > 0) {
pending_real_buffers++;
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@danepowell"]
+84
View File
@@ -0,0 +1,84 @@
#include "icnt86.h"
#include "esphome/core/log.h"
namespace esphome::icnt86 {
static const char *const TAG = "icnt86";
static constexpr uint16_t REG_TOUCH_NUM = 0x1001;
static constexpr uint16_t REG_POINT1 = 0x1002;
static constexpr uint8_t MAX_TOUCHES = 5;
static constexpr uint8_t POINT_SIZE = 7;
void ICNT86Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up icnt86 Touchscreen...");
// Register interrupt pin
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
// Perform reset if necessary
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();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = this->display_->get_native_height();
}
}
void ICNT86Touchscreen::update_touches() {
uint8_t buf[MAX_TOUCHES * POINT_SIZE] = {0};
uint8_t mask[1] = {0x00};
if (this->read_register16(REG_TOUCH_NUM, buf, 1) != i2c::ERROR_OK) {
this->status_set_warning();
this->skip_update_ = true;
ESP_LOGW(TAG, "Failed to read touch count");
return;
}
uint8_t touch_count = buf[0];
if (touch_count == 0x00 || touch_count > MAX_TOUCHES) { // No new touch
this->status_clear_warning();
return;
}
if (this->read_register16(REG_POINT1, buf, touch_count * POINT_SIZE) != i2c::ERROR_OK) {
this->status_set_warning();
this->skip_update_ = true;
ESP_LOGW(TAG, "Failed to read touch points");
return;
}
this->write_register16(REG_TOUCH_NUM, mask, 1);
ESP_LOGV(TAG, "Touch count: %d", touch_count);
this->status_clear_warning();
for (uint8_t i = 0; i < touch_count; i++) {
uint16_t x = ((uint16_t) buf[2 + 7 * i] << 8) + buf[1 + 7 * i];
uint16_t y = ((uint16_t) buf[4 + 7 * i] << 8) + buf[3 + 7 * i];
uint8_t pressure = buf[5 + 7 * i];
uint8_t touch_id = buf[6 + 7 * i];
// A zero-pressure report just means this point is no longer touched; skipping it here leaves is_touched_
// false (when no other point is active) so send_touches_() reports the release as normal.
if (pressure != 0) {
this->add_raw_touch_position_(touch_id, x, y, pressure);
}
}
}
void ICNT86Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "icnt86 Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
}
} // namespace esphome::icnt86
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome::icnt86 {
class ICNT86Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void update_touches() override;
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{nullptr};
};
} // namespace esphome::icnt86
+40
View File
@@ -0,0 +1,40 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from esphome.types import ConfigType
CODEOWNERS = ["@danepowell"]
DEPENDENCIES = ["i2c"]
icnt86_ns = cg.esphome_ns.namespace("icnt86")
ICNT86Touchscreen = icnt86_ns.class_(
"ICNT86Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONFIG_SCHEMA = touchscreen.touchscreen_schema("250ms").extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ICNT86Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
).extend(i2c.i2c_device_schema(0x48))
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN):
cg.add(
var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin_config))
)
if reset_pin_config := config.get(CONF_RESET_PIN):
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin_config)))
+85 -61
View File
@@ -5,83 +5,107 @@
namespace esphome::image {
// Walks the clipped image range with rows (ROWS_OUTER) or columns as the outer
// loop. The coordinate selection folds at compile time, and the pixel body must
// be ESPHOME_ALWAYS_INLINE: at -Os GCC otherwise leaves it out of line and calls
// it once per pixel.
template<bool ROWS_OUTER, typename F>
static void for_each_pixel(int x_start, int x_end, int y_start, int y_end, F &&f) {
const int outer_start = ROWS_OUTER ? y_start : x_start;
const int outer_end = ROWS_OUTER ? y_end : x_end;
const int inner_start = ROWS_OUTER ? x_start : y_start;
const int inner_end = ROWS_OUTER ? x_end : y_end;
for (int outer = outer_start; outer < outer_end; outer++) {
for (int inner = inner_start; inner < inner_end; inner++) {
f(ROWS_OUTER ? inner : outer, ROWS_OUTER ? outer : inner);
}
}
}
void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
int img_x0 = 0;
int img_y0 = 0;
int w = width_;
int h = height_;
int x_start = 0;
int y_start = 0;
int x_end = width_;
int y_end = height_;
auto clipping = display->get_clipping();
if (clipping.is_set()) {
if (clipping.x > x)
img_x0 += clipping.x - x;
x_start += clipping.x - x;
if (clipping.y > y)
img_y0 += clipping.y - y;
if (w > clipping.x2() - x)
w = clipping.x2() - x;
if (h > clipping.y2() - y)
h = clipping.y2() - y;
y_start += clipping.y - y;
if (x_end > clipping.x2() - x)
x_end = clipping.x2() - x;
if (y_end > clipping.y2() - y)
y_end = clipping.y2() - y;
}
switch (type_) {
case IMAGE_TYPE_BINARY: {
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!this->transparency_) {
display->draw_pixel_at(x + img_x, y + img_y, color_off);
}
// Pixel data and frame buffers are row-major, so walking rows keeps the
// image read and the frame buffer write sequential. When the display swaps
// the axes before writing, walking columns keeps the write sequential
// instead; a strided write costs more than a strided read.
#ifdef USE_PSRAM
if (display->pixel_axes_swapped()) {
this->draw_<false>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
return;
}
#endif
// Without PSRAM the frame buffer is in uncached RAM, where the strided write
// is free, so one copy of the loops is enough.
this->draw_<true>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
}
template<bool ROWS_OUTER>
void Image::draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int x_start, int x_end,
int y_start, int y_end) {
// Local copies: draw_pixel_at() is an opaque virtual call, so members read
// in the loop would otherwise be reloaded every pixel.
const Transparency transparency = this->transparency_;
switch (this->type_) {
case IMAGE_TYPE_BINARY:
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE {
if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (transparency == TRANSPARENCY_OPAQUE) {
display->draw_pixel_at(x + img_x, y + img_y, color_off);
}
}
});
break;
case IMAGE_TYPE_GRAYSCALE: {
const int width = this->width_;
const uint8_t *const data = this->data_start_;
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE {
const uint8_t gray = progmem_read_byte(data + img_x + img_y * width);
Color color = Color(gray, gray, gray, 0xFF);
if (transparency == TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) {
return; // transparent pixel, skip drawing
}
} else if (transparency == TRANSPARENCY_ALPHA_CHANNEL) {
// gray is the alpha: blend from color_off to color_on, drawn opaque
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
Color::blend_channel(color_off.g, color_on.g, gray),
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
}
display->draw_pixel_at(x + img_x, y + img_y, color);
});
break;
}
case IMAGE_TYPE_GRAYSCALE:
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
const uint32_t pos = (img_x + img_y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
Color color = Color(gray, gray, gray, 0xFF);
switch (this->transparency_) {
case TRANSPARENCY_CHROMA_KEY:
if (gray == 1) {
continue; // skip drawing
}
break;
case TRANSPARENCY_ALPHA_CHANNEL: {
auto on = (float) gray / 255.0f;
auto off = 1.0f - on;
// blend color_on and color_off
color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off,
color_on.b * on + color_off.b * off, 0xFF);
break;
}
default:
break;
}
case IMAGE_TYPE_RGB565:
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE {
auto color = this->get_rgb565_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
break;
case IMAGE_TYPE_RGB565:
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
auto color = this->get_rgb565_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
});
break;
case IMAGE_TYPE_RGB:
for (int img_x = img_x0; img_x < w; img_x++) {
for (int img_y = img_y0; img_y < h; img_y++) {
auto color = this->get_rgb_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE {
auto color = this->get_rgb_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
});
break;
}
}
+4 -1
View File
@@ -43,6 +43,10 @@ class Image : public display::BaseImage {
lv_image_dsc_t *get_lv_image_dsc();
#endif
protected:
/// Per-pixel draw of the clipped range, rows (true) or columns (false) outer.
template<bool ROWS_OUTER>
void draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int x_start, int x_end,
int y_start, int y_end);
bool get_binary_pixel_(int x, int y) const;
Color get_rgb_pixel_(int x, int y) const;
Color get_rgb565_pixel_(int x, int y) const;
@@ -54,7 +58,6 @@ class Image : public display::BaseImage {
const uint8_t *data_start_;
Transparency transparency_;
size_t bpp_{};
size_t stride_{};
#ifdef USE_LVGL
lv_img_dsc_t dsc_{};
#endif
+4 -2
View File
@@ -38,9 +38,11 @@ def _process_next_url(url: str) -> str:
return url
async def setup_improv_core(var: MockObj, config: ConfigType, component: str) -> None:
async def setup_improv_core(var: MockObj, config: ConfigType) -> None:
if next_url := config.get(CONF_NEXT_URL):
cg.add(var.set_next_url(_process_next_url(next_url)))
cg.add_define(f"USE_{component.upper()}_NEXT_URL")
# One define for all transports: next_url_ is per object, so a transport
# configured without next_url: calls add_next_url_ and appends nothing.
cg.add_define("USE_IMPROV_NEXT_URL")
cg.add_library("improv/Improv", "1.2.7")
@@ -8,7 +8,7 @@
namespace esphome::improv_base {
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#ifdef USE_IMPROV_NEXT_URL
static const char *const TAG = "improv_base";
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
+3 -3
View File
@@ -3,7 +3,7 @@
#include <cstddef>
#include "esphome/core/defines.h"
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#ifdef USE_IMPROV_NEXT_URL
#include <improv.h>
#endif
@@ -11,12 +11,12 @@ namespace esphome::improv_base {
class ImprovBase {
public:
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#ifdef USE_IMPROV_NEXT_URL
void set_next_url(const char *next_url) { this->next_url_ = next_url; }
#endif
protected:
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#ifdef USE_IMPROV_NEXT_URL
/// Format next_url_ into buffer, replacing placeholders. Returns length written.
size_t get_formatted_next_url_(char *buffer, size_t buffer_size);
/// Append the formatted next_url to the RPC response, warning if it does not fit.
@@ -1,14 +1,41 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor, esp32_ble, improv_base, output
from esphome.components.esp32_ble import BTLoggers
from esphome.components import binary_sensor, improv_base, output
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, CONF_TRIGGER_ID
from esphome.const import (
CONF_ID,
CONF_ON_START,
CONF_ON_STATE,
CONF_TRIGGER_ID,
PLATFORM_ESP32,
)
from esphome.core import CORE
from esphome.types import ConfigType
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
# The BLE GATT server component that hosts the Improv service, per target
# platform. improv_ble itself is platform neutral; supporting another chip
# means adding its BLE server component here and the matching backend in
# improv_ble_component.cpp. Doubles as the platform gate below, so an
# unsupported chip is rejected in validation rather than at link time.
BLE_SERVER_BACKENDS: dict[str, str] = {
PLATFORM_ESP32: "esp32_ble_server",
}
def AUTO_LOAD() -> list[str]:
auto_load = ["improv_base"]
if backend := BLE_SERVER_BACKENDS.get(CORE.target_platform):
auto_load.append(backend)
return auto_load
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["wifi", "esp32"]
DEPENDENCIES = ["wifi"]
# Legacy top-level YAML key that routes here; esphome/loader.py and
# esphome/config.py handle the warning and the key rename.
ALIASES = ["esp32_improv"]
ALIAS_REMOVAL_VERSION = "2027.4.0"
CONF_AUTHORIZED_DURATION = "authorized_duration"
CONF_AUTHORIZER = "authorizer"
@@ -29,29 +56,29 @@ improv_ns = cg.esphome_ns.namespace("improv")
Error = improv_ns.enum("Error")
State = improv_ns.enum("State")
esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
ESP32ImprovComponent = esp32_improv_ns.class_("ESP32ImprovComponent", cg.Component)
ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisionedTrigger", automation.Trigger.template()
improv_ble_ns = cg.esphome_ns.namespace("improv_ble")
ImprovBLEComponent = improv_ble_ns.class_("ImprovBLEComponent", cg.Component)
ImprovBLEProvisionedTrigger = improv_ble_ns.class_(
"ImprovBLEProvisionedTrigger", automation.Trigger.template()
)
ESP32ImprovProvisioningTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisioningTrigger", automation.Trigger.template()
ImprovBLEProvisioningTrigger = improv_ble_ns.class_(
"ImprovBLEProvisioningTrigger", automation.Trigger.template()
)
ESP32ImprovStartTrigger = esp32_improv_ns.class_(
"ESP32ImprovStartTrigger", automation.Trigger.template()
ImprovBLEStartTrigger = improv_ble_ns.class_(
"ImprovBLEStartTrigger", automation.Trigger.template()
)
ESP32ImprovStateTrigger = esp32_improv_ns.class_(
"ESP32ImprovStateTrigger", automation.Trigger.template()
ImprovBLEStateTrigger = improv_ble_ns.class_(
"ImprovBLEStateTrigger", automation.Trigger.template()
)
ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
"ESP32ImprovStoppedTrigger", automation.Trigger.template()
ImprovBLEStoppedTrigger = improv_ble_ns.class_(
"ImprovBLEStoppedTrigger", automation.Trigger.template()
)
CONFIG_SCHEMA = (
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
cv.GenerateID(): cv.declare_id(ImprovBLEComponent),
cv.Required(CONF_AUTHORIZER): cv.Any(
cv.none, cv.use_id(binary_sensor.BinarySensor)
),
@@ -68,55 +95,60 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisionedTrigger
ImprovBLEProvisionedTrigger
),
}
),
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisioningTrigger
ImprovBLEProvisioningTrigger
),
}
),
cv.Optional(CONF_ON_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStartTrigger
ImprovBLEStartTrigger
),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStateTrigger
ImprovBLEStateTrigger
),
}
),
cv.Optional(CONF_ON_STOP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStoppedTrigger
ImprovBLEStoppedTrigger
),
}
),
}
)
.extend(improv_base.IMPROV_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on(list(BLE_SERVER_BACKENDS)),
)
async def to_code(config: ConfigType) -> None:
# ESP32 backend setup: the platform gate above means this is the only backend
# that can reach to_code. Make it conditional when a second one is added.
from esphome.components import esp32_ble
# Register the loggers this component needs
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
esp32_ble.register_bt_logger(esp32_ble.BTLoggers.GATT, esp32_ble.BTLoggers.SMP)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add_define("USE_IMPROV")
cg.add_define("USE_IMPROV_BLE")
await improv_base.setup_improv_core(var, config, "esp32_improv")
await improv_base.setup_improv_core(var, config)
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
@@ -155,4 +187,4 @@ async def to_code(config: ConfigType) -> None:
await automation.build_automation(trigger, [], conf)
use_state_callback = True
if use_state_callback:
cg.add_define("USE_ESP32_IMPROV_STATE_CALLBACK")
cg.add_define("USE_IMPROV_BLE_STATE_CALLBACK")
@@ -1,17 +1,17 @@
#pragma once
#ifdef USE_ESP32
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#include "esp32_improv_component.h"
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
#include "improv_ble_component.h"
#include "esphome/core/automation.h"
#include <improv.h>
namespace esphome::esp32_improv {
namespace esphome::improv_ble {
class ESP32ImprovProvisionedTrigger final : public Trigger<> {
class ImprovBLEProvisionedTrigger final : public Trigger<> {
public:
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEProvisionedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) {
this->trigger();
@@ -20,12 +20,12 @@ class ESP32ImprovProvisionedTrigger final : public Trigger<> {
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
class ESP32ImprovProvisioningTrigger final : public Trigger<> {
class ImprovBLEProvisioningTrigger final : public Trigger<> {
public:
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEProvisioningTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) {
this->trigger();
@@ -34,12 +34,12 @@ class ESP32ImprovProvisioningTrigger final : public Trigger<> {
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
class ESP32ImprovStartTrigger final : public Trigger<> {
class ImprovBLEStartTrigger final : public Trigger<> {
public:
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEStartTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
!this->parent_->is_failed()) {
@@ -49,12 +49,12 @@ class ESP32ImprovStartTrigger final : public Trigger<> {
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
class ESP32ImprovStateTrigger final : public Trigger<improv::State, improv::Error> {
class ImprovBLEStateTrigger final : public Trigger<improv::State, improv::Error> {
public:
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEStateTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (!this->parent_->is_failed()) {
this->trigger(state, error);
@@ -63,12 +63,12 @@ class ESP32ImprovStateTrigger final : public Trigger<improv::State, improv::Erro
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
class ESP32ImprovStoppedTrigger final : public Trigger<> {
class ImprovBLEStoppedTrigger final : public Trigger<> {
public:
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEStoppedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) {
this->trigger();
@@ -77,10 +77,10 @@ class ESP32ImprovStoppedTrigger final : public Trigger<> {
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
} // namespace esphome::esp32_improv
} // namespace esphome::improv_ble
#endif
#endif
@@ -1,10 +1,7 @@
#include "esp32_improv_component.h"
#include "improv_ble_component.h"
#include <array>
#include "esphome/components/bytebuffer/bytebuffer.h"
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble_server/ble_2902.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -15,11 +12,15 @@
#ifdef USE_ESP32
namespace esphome::esp32_improv {
#include "esphome/components/bytebuffer/bytebuffer.h"
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble_server/ble_2902.h"
namespace esphome::improv_ble {
using namespace bytebuffer;
static const char *const TAG = "esp32_improv.component";
static const char *const TAG = "improv_ble.component";
static constexpr size_t IMPROV_MAX_LOG_BYTES = 128;
static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
// command + data length + trailing byte
@@ -38,9 +39,9 @@ static constexpr uint8_t IMPROV_SERVICE_DATA_SIZE = 8;
static constexpr uint8_t IMPROV_PROTOCOL_ID_1 = 0x77; // 'P' << 1 | 'R' >> 7
static constexpr uint8_t IMPROV_PROTOCOL_ID_2 = 0x46; // 'I' << 1 | 'M' >> 7
ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; }
ImprovBLEComponent::ImprovBLEComponent() { global_improv_component = this; }
void ESP32ImprovComponent::setup() {
void ImprovBLEComponent::setup() {
#ifdef USE_BINARY_SENSOR
if (this->authorizer_ != nullptr) {
this->authorizer_->add_on_state_callback([this](bool state) {
@@ -66,7 +67,7 @@ void ESP32ImprovComponent::setup() {
this->disable_loop();
}
void ESP32ImprovComponent::setup_characteristics() {
void ImprovBLEComponent::setup_characteristics() {
this->status_ = this->service_->create_characteristic(
improv::STATUS_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor *status_descriptor = new BLE2902();
@@ -104,11 +105,11 @@ void ESP32ImprovComponent::setup_characteristics() {
this->setup_complete_ = true;
}
void ESP32ImprovComponent::loop() {
void ImprovBLEComponent::loop() {
if (!global_ble_server->is_running()) {
if (this->state_ != improv::STATE_STOPPED) {
this->state_ = improv::STATE_STOPPED;
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
@@ -200,23 +201,19 @@ void ESP32ImprovComponent::loop() {
}
}
void ESP32ImprovComponent::set_status_indicator_state_(bool state) {
void ImprovBLEComponent::set_status_indicator_state_(bool state) {
#ifdef USE_OUTPUT
if (this->status_indicator_ == nullptr)
return;
if (this->status_indicator_state_ == state)
return;
this->status_indicator_state_ = state;
if (state) {
this->status_indicator_->turn_on();
} else {
this->status_indicator_->turn_off();
}
this->status_indicator_->set_state(state);
#endif
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
const char *ESP32ImprovComponent::state_to_string_(improv::State state) {
const char *ImprovBLEComponent::state_to_string_(improv::State state) {
switch (state) {
case improv::STATE_STOPPED:
return "STOPPED";
@@ -234,7 +231,7 @@ const char *ESP32ImprovComponent::state_to_string_(improv::State state) {
}
#endif
bool ESP32ImprovComponent::check_identify_() {
bool ImprovBLEComponent::check_identify_() {
uint32_t now = millis();
bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_;
@@ -246,7 +243,7 @@ bool ESP32ImprovComponent::check_identify_() {
return identify;
}
void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertising) {
void ImprovBLEComponent::set_state_(improv::State state, bool update_advertising) {
// Skip if state hasn't changed
if (this->state_ == state) {
return;
@@ -274,12 +271,12 @@ void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertisi
// Advertise the new state via service data
this->advertise_service_data_();
}
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
void ESP32ImprovComponent::set_error_(improv::Error error) {
void ImprovBLEComponent::set_error_(improv::Error error) {
if (error != improv::ERROR_NONE) {
ESP_LOGE(TAG, "Error: %d", error);
}
@@ -295,14 +292,14 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
}
}
void ESP32ImprovComponent::send_response_(std::span<const uint8_t> response) {
void ImprovBLEComponent::send_response_(std::span<const uint8_t> response) {
// The BLE characteristic owns its value, so one exact-size copy is required here
this->rpc_response_->set_value(std::vector<uint8_t>(response.begin(), response.end()));
if (this->state_ != improv::STATE_STOPPED)
this->rpc_response_->notify();
}
void ESP32ImprovComponent::start() {
void ImprovBLEComponent::start() {
if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
return;
@@ -320,7 +317,7 @@ void ESP32ImprovComponent::start() {
this->enable_loop();
}
void ESP32ImprovComponent::stop() {
void ImprovBLEComponent::stop() {
this->should_start_ = false;
// Wait before stopping the service to ensure all BLE clients see the state change.
// This prevents clients from repeatedly reconnecting and wasting resources by allowing
@@ -335,10 +332,10 @@ void ESP32ImprovComponent::stop() {
});
}
float ESP32ImprovComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
float ImprovBLEComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void ESP32ImprovComponent::dump_config() {
ESP_LOGCONFIG(TAG, "ESP32 Improv:");
void ImprovBLEComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Improv BLE:");
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_);
#endif
@@ -347,7 +344,7 @@ void ESP32ImprovComponent::dump_config() {
#endif
}
void ESP32ImprovComponent::process_incoming_data_() {
void ImprovBLEComponent::process_incoming_data_() {
if (this->incoming_data_.size() < 3)
return;
uint8_t length = this->incoming_data_[1];
@@ -422,7 +419,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
}
}
void ESP32ImprovComponent::on_wifi_connect_timeout_() {
void ImprovBLEComponent::on_wifi_connect_timeout_() {
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
this->set_state_(improv::STATE_AUTHORIZED);
#ifdef USE_BINARY_SENSOR
@@ -433,7 +430,7 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
wifi::global_wifi_component->clear_sta();
}
void ESP32ImprovComponent::check_wifi_connection_() {
void ImprovBLEComponent::check_wifi_connection_() {
if (!wifi::global_wifi_component->is_connected()) {
return;
}
@@ -447,7 +444,7 @@ void ESP32ImprovComponent::check_wifi_connection_() {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, improv::WIFI_SETTINGS);
#ifdef USE_ESP32_IMPROV_NEXT_URL
#ifdef USE_IMPROV_NEXT_URL
// Add next_url if configured (should be first per Improv BLE spec)
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
#endif
@@ -480,7 +477,7 @@ void ESP32ImprovComponent::check_wifi_connection_() {
this->stop();
}
void ESP32ImprovComponent::advertise_service_data_() {
void ImprovBLEComponent::advertise_service_data_() {
uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
service_data[0] = IMPROV_PROTOCOL_ID_1; // PR
service_data[1] = IMPROV_PROTOCOL_ID_2; // IM
@@ -499,7 +496,7 @@ void ESP32ImprovComponent::advertise_service_data_() {
esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>(service_data), false);
}
void ESP32ImprovComponent::update_advertising_type_() {
void ImprovBLEComponent::update_advertising_type_() {
uint32_t now = App.get_loop_component_start_time();
// If we're advertising the device name and it's been more than NAME_ADVERTISING_DURATION, switch back to service data
@@ -524,21 +521,21 @@ void ESP32ImprovComponent::update_advertising_type_() {
}
}
void ESP32ImprovComponent::request_advertising_() {
void ImprovBLEComponent::request_advertising_() {
if (this->advertising_requested_)
return;
this->advertising_requested_ = true;
esp32_ble::global_ble->advertising_start();
}
void ESP32ImprovComponent::release_advertising_() {
void ImprovBLEComponent::release_advertising_() {
if (!this->advertising_requested_)
return;
this->advertising_requested_ = false;
esp32_ble::global_ble->advertising_stop();
}
improv::State ESP32ImprovComponent::get_initial_state_() const {
improv::State ImprovBLEComponent::get_initial_state_() const {
#ifdef USE_BINARY_SENSOR
// If we have an authorizer, start in awaiting authorization state
return this->authorizer_ == nullptr ? improv::STATE_AUTHORIZED : improv::STATE_AWAITING_AUTHORIZATION;
@@ -548,8 +545,8 @@ improv::State ESP32ImprovComponent::get_initial_state_() const {
#endif
}
ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
ImprovBLEComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::esp32_improv
} // namespace esphome::improv_ble
#endif
@@ -5,12 +5,10 @@
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
#include "esphome/components/esp32_ble_server/ble_server.h"
#include "esphome/components/improv_base/improv_base.h"
#include "esphome/components/wifi/wifi_component.h"
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
#include "esphome/core/automation.h"
#endif
@@ -25,17 +23,23 @@
#include <span>
#include <vector>
// ESP-IDF is currently the only target platform with a BLE GATT server, so it is
// the only backend this component has. The Python side keeps the platform table
// (BLE_SERVER_BACKENDS in __init__.py); a second backend adds another arm here.
#ifdef USE_ESP32
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
#include "esphome/components/esp32_ble_server/ble_server.h"
#include <improv.h>
namespace esphome::esp32_improv {
namespace esphome::improv_ble {
using namespace esp32_ble_server;
class ESP32ImprovComponent final : public Component, public improv_base::ImprovBase {
class ImprovBLEComponent final : public Component, public improv_base::ImprovBase {
public:
ESP32ImprovComponent();
ImprovBLEComponent();
void dump_config() override;
void loop() override;
void setup() override;
@@ -47,7 +51,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
bool should_start() const { return this->should_start_; }
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
@@ -97,7 +101,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
improv::State state_{improv::STATE_STOPPED};
improv::Error error_state_{improv::ERROR_NONE};
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
CallbackManager<void(improv::State, improv::Error)> state_callback_{};
#endif
@@ -125,8 +129,8 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ESP32ImprovComponent *global_improv_component;
extern ImprovBLEComponent *global_improv_component;
} // namespace esphome::esp32_improv
} // namespace esphome::improv_ble
#endif
+1 -1
View File
@@ -70,7 +70,7 @@ FINAL_VALIDATE_SCHEMA = validate_transport
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await improv_base.setup_improv_core(var, config, "improv_serial")
await improv_base.setup_improv_core(var, config)
cg.add_define("USE_IMPROV_SERIAL")
if (uart_id := config.get(CONF_UART_ID)) is not None:
cg.add(var.set_uart(await cg.get_variable(uart_id)))
@@ -208,7 +208,7 @@ void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &buil
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, command);
#ifdef USE_IMPROV_SERIAL_NEXT_URL
#ifdef USE_IMPROV_NEXT_URL
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
#endif
#ifdef USE_WEBSERVER
Loaded 100 of 357 files, more files were not shown because too many files have changed in this diff. Show more