Compare commits

...
Author SHA1 Message Date
J. Nick Koston 7c992d1ad3 [at581x] Apply configured false and zero values in the settings action 2026-09-22 23:59:43 +01:00
luar123 79532f2a85 [zigbee] Require mbedTLS extras for Zigbee crypto (#19457) 2026-09-22 22:48:50 +01:00
dependabot[bot] 2b805b897b Bump platformdirs from 4.11.10 to 4.11.11 (#19465) 2026-09-22 22:08:24 +01:00
dependabot[bot] 44cf2b594a Bump filelock from 4.0.0 to 4.0.1 (#19464) 2026-09-22 22:08:00 +01:00
esphome[bot] f4aaa595f8 Bump aioesphomeapi from 46.4.1 to 46.5.0 (#19463) 2026-09-22 17:59:19 +00:00
dependabot[bot] a78e955691 Bump esphome/workflows/.github/workflows/stale.yml from a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 to cc3e76de337dc59bc1cba8da58d963cd23b873f1 (#19444)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 22:42:45 +01:00
dependabot[bot] 632f63b34b Bump github/codeql-action/init from 4.38.0 to 4.38.1 (#19445)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 22:42:33 +01:00
dependabot[bot] cd50f949ae Bump github/codeql-action/analyze from 4.38.0 to 4.38.1 (#19447)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 22:42:25 +01:00
dependabot[bot] 352121d60f Bump esphome/workflows/.github/workflows/lock.yml from 2026.8.1 to 2026.9.0 (#19448)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 22:42:02 +01:00
dependabot[bot] 3b4a6f21f9 Bump filelock from 3.32.6 to 4.0.0 (#19442)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 22:41:52 +01:00
dependabot[bot]andesphome[bot] cd012b57ed Bump ruff from 0.16.7 to 0.16.8 (#19441)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 22:41:36 +01:00
dependabot[bot] 8f3f25b832 Bump codecov/codecov-action from 7.1.0 to 7.1.1 (#19446)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 22:41:22 +01:00
dependabot[bot] 81dd57b402 Bump platformdirs from 4.11.8 to 4.11.10 (#19443)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 22:41:00 +01:00
Iago VeigaandClyde Stubbs 5764a72f1b [syslog] Add configurable RFC 3164 and RFC 5424 formats (#18993)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-09-22 07:26:06 +10:00
esphome[bot] 7a27d5dde1 [ci] Refresh integration test durations (#19435) 2026-09-21 11:45:23 +01:00
Jonathan SwobodaandClaude Fable 5.1 a00d241cad [esp32] Disable newlib nano printf formatting on ESP32-C2 (#19418)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 18:08:56 -04:00
Jesse Hills c10c06eeb5 [ci] Add code-review agent skill for GitHub Copilot (#19370) 2026-09-20 00:44:33 +12:00
rwrozelleandClaude Sonnet 5 f94661561d [socket] Fix set_sockaddr() accepting malformed IPv4 addresses (#19394)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 18:10:11 -05:00
Xiretza a53efcf646 [core] fix capitalisation of "pin" (#19405) 2026-09-18 18:30:35 -04:00
Keith BurzinskiandClaude Opus 5 623aec2984 [mdns] Advertise ota_signed when signed OTA verification is compiled in (#19407)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 17:18:44 -05:00
dependabot[bot] aba49c3d4f Bump docker/build-push-action from 7.3.0 to 7.4.0 in /.github/actions/build-image (#19403)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 14:32:42 -05:00
dependabot[bot] 28189ea026 Bump docker/setup-buildx-action from 4.3.0 to 4.4.0 in the docker-actions group (#19404)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 14:32:33 -05:00
dependabot[bot] f6897f2257 Bump codecov/codecov-action from 7.0.0 to 7.1.0 (#19382)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-17 14:57:00 -05:00
9309cf96b9 [web_server_idf] Close stalled EventSource sessions via HTTPD (#17800)
Co-authored-by: Jeroen Jansen <jeroen@Jeroens-MacBook-Air.local>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-09-17 10:38:56 -05:00
J. Nick Koston a63dc0d9c9 [esphome] Keep the OTA encryption key without the api server so safe mode uploads work (#19349) 2026-09-17 10:20:41 -05:00
J. Nick Koston f433f99e90 [esp32_hosted] Fix ESP32-P4 build without wifi, espnow or BLE (#19374) 2026-09-17 14:57:59 +00:00
J. Nick Koston 1dbbf3f45a [espidf] Switch the Windows console to UTF-8 while idf.py runs (#19352) 2026-09-17 09:40:22 -05:00
J. Nick Koston 91ab229086 [core] Lint bare string literal ternaries in ESP_LOG arguments (#18908) 2026-09-17 09:17:08 -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
389 changed files with 6766 additions and 2327 deletions

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
---
name: code-review
description: Review guidance for ESPHome pull requests. Use this when reviewing a pull request that changes ESPHome Python, C++, or component code, to check it against the project's coding conventions, embedded-systems memory rules, testing requirements, and breaking-change policy.
---
# Reviewing ESPHome pull requests
ESPHome parses YAML into C++ firmware for memory-constrained microcontrollers
(ESP32, ESP8266, RP2040, LibreTiny). Review changes with that in mind: RAM and
flash are scarce, and code runs unattended for months.
`AGENTS.md` in the repository root is the full contributor guide and the
authority when it disagrees with this summary. The developer documentation at
https://developers.esphome.io explains the component lifecycle and the reasoning
behind these rules. This skill lists the concrete things worth flagging in a
review; read `AGENTS.md` for the detail behind any item.
Only raise findings that the diff actually introduces or changes. Do not ask for
drive-by cleanup of pre-existing code the PR did not touch.
## Memory and embedded constraints (highest value)
Heap allocation after `setup()` is treated as a reliability bug, not a
performance nit, because it fragments a small shared heap. Flag:
- New heap allocation on a hot path or after setup that could be avoided.
- `std::vector` where the size is known at compile time (use `std::array`, or
`StaticVector<T, N>` when a `push_back` API is needed) or fixed at runtime
init (use `FixedVector<T>`).
- Listener / child-entity registration lists stored as `std::vector`; these have
a compile-time-known count and should use `cg.slot_counter()` plus
`StaticVector`.
- `std::vector<uint8_t>` for a byte buffer that never grows: prefer
`std::unique_ptr<uint8_t[]>` or `std::array`.
- `std::map` / `std::set` / `std::unordered_map` for small datasets (1-16
elements): a `std::vector` of a small struct with linear search is lighter.
- `std::deque` anywhere: it allocates 512-byte blocks and should be avoided.
- `std::string` storing a value set once from config: prefer `StringRef` (the
literal already lives in flash).
- `std::string` / `std::to_string` / string-returning helpers on hot paths where
a buffer or view API exists.
## C++ conventions
- Include what you use: a file referencing a symbol must include the header that
declares it, even if it currently arrives transitively. New or changed uses of
a symbol need the matching include.
- Prefix all member access with `this->`.
- Naming: `lower_snake_case` for functions/methods/variables, `UpperCamelCase`
for classes/structs/enums, `UPPER_SNAKE_CASE` for namespace-scope constants,
trailing underscore on protected/private fields.
- `enum class` values must be prefixed with the enum name in `UPPER_SNAKE_CASE`
(e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Bare names like `SUCCESS`,
`FAIL`, or `OK` collide with SDK macros on some platforms and break the build.
- Prefer `const`/`enum` over `#define`; `#define` is only for conditional
compilation and code-generation sizes.
- Never call `millis()` in a `loop()` body; use
`App.get_loop_component_start_time()`. A rate-limit gate below ~16 ms (the loop
period) does nothing.
- Pick the timing primitive by cadence: gated `loop()` under 250 ms,
`set_interval` at 500 ms and above.
- Do not override a base method to return the value it already returns (e.g.
`get_setup_priority()` returning `setup_priority::DATA`).
- Wrap string literals passed as printf `%s` args in `LOG_STR_LITERAL()`.
- Required, invariant dependencies should be constructor parameters, not setters.
- Callback registration methods must be templated (`template<typename F>`), not
typed as `std::function`, so lightweight forwarders avoid a heap allocation.
- Two-space indent, `using` over `typedef`, wrap at 120 columns.
## Python conventions
- Type-annotate every new function signature (params and return), new dataclass
fields, and new module-level variables. Import `ConfigType` from
`esphome.types`.
- Use the walrus operator to avoid a double lookup, e.g.
`if (blah := config.get(CONF_BLAH)) is not None:`.
- Reuse existing validators from `config_validation.py` (`cv.rename_key`,
`cv.has_exactly_one_key`, etc.) via `cv.All(...)` instead of hand-rolling.
- `esphome/const.py` is frozen: no new `CONF_` constants there. Define them in
the component's own `.py`, or in `esphome/components/const/__init__.py` when
shared. The same constant defined in three or more component files fails CI.
- State that must persist during code generation goes in `CORE.data` namespaced
under the component `DOMAIN` (a `@dataclass`), not module-level mutable globals.
- Prefer callback-based triggers via `build_callback_automation()`; only use a
`Trigger<Ts...>` subclass when the forwarder needs mutable state.
## Testing and coverage
- New and changed lines and branches need test coverage, including defensive
early-returns, error paths, and no-op guards. A mocked-out function is not
covered; exercise the real call path too.
- Component YAML tests live in `tests/components/<component>/`. Never define
buses (uart, i2c, spi, modbus) directly in a test file: pull them from
`tests/test_build_components/common/` through dict-style `packages:` so CI can
group builds. List-style packages or top-level merge keys block grouping.
- Config-only checks use the `validate.*.yaml` prefix; compiled checks use
`test.*.yaml`.
## Breaking changes and public API
- Base classes under `esphome/core/` and documented config options are public
API. Undocumented `public` members of a component are internal.
- A breaking change needs justification, a migration path in the PR description,
and a deprecation window where feasible (`ESPDEPRECATED` in C++,
`cv.rename_key(..., removed_in=...)` in Python). Changing a codegen-injected
lambda signature is not a breaking change.
## Process and PR hygiene
- PR titles start with a `[tag]` prefix: the component name (e.g. `[uart] ...`)
or `[core]` for shared code.
- Prose in docs, comments, and commit messages should be plain English. Keep
inline comments short and only where the code is not self-explanatory; do not
restate what the code says.
- Verify the PR fills out `.github/PULL_REQUEST_TEMPLATE.md` and adds
`CODEOWNERS` entries for a new component.
File renamed without changes.
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+2 -2
View File
@@ -42,7 +42,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -67,7 +67,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
+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
+2 -2
View File
@@ -67,7 +67,7 @@ jobs:
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Determine tag and whether to push
id: tag
@@ -159,7 +159,7 @@ jobs:
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to the GitHub container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
+5 -5
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
@@ -321,7 +321,7 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -350,7 +350,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Report empty upload to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
run_command: empty-upload
force: true
@@ -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@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
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@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
permissions:
issues: write # issues.lock on closed issues
pull-requests: write # issues.lock on closed pull requests
uses: esphome/workflows/.github/workflows/lock.yml@0fdd5e311b7e744069166696072a1a9cbc5fbeb6 # 2026.8.1
uses: esphome/workflows/.github/workflows/lock.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # 2026.9.0
+2 -2
View File
@@ -123,7 +123,7 @@ jobs:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to docker hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
@@ -202,7 +202,7 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 # main
uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
+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.8
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
+24 -2
View File
@@ -29,6 +29,29 @@ static const char *const TAG = "api";
// APIServer
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_API_NOISE
static constexpr uint32_t NOISE_PSK_PREF_HASH = 88491486UL;
#endif
#if defined(USE_API_NOISE) && defined(USE_OTA_ENCRYPTION_PROVISIONED)
bool load_saved_noise_psk(noise::psk_t &out) {
SavedNoisePsk saved;
#ifdef USE_PREFERENCE_KEY_LOOKUP
const bool loaded =
global_preferences->load_from_key(NOISE_PSK_PREF_HASH, reinterpret_cast<uint8_t *>(&saved), sizeof(saved));
#else
// Slot backends need the reservation walk; it only lands on the record when the reservations before
// it match a normal boot, otherwise the type checked checksum fails the load
const bool loaded = global_preferences->make_preference<SavedNoisePsk>(NOISE_PSK_PREF_HASH, true).load(&saved);
#endif
// The all-zeros record means no key
if (!loaded || noise::NoiseContext::is_all_zeros(saved.psk))
return false;
out = saved.psk;
return true;
}
#endif
APIServer::APIServer() { global_api_server = this; }
void APIServer::socket_failed_(const LogString *msg) {
@@ -43,8 +66,7 @@ void APIServer::setup() {
#ifdef USE_API_NOISE
// Always reserve the slot: flash preferences are positional on esp8266, so
// a yaml key build must keep the layout of a runtime key build
uint32_t hash = 88491486UL;
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(NOISE_PSK_PREF_HASH, true);
#ifndef USE_API_NOISE_PSK_FROM_YAML
// A cleared record loads fine but holds no key
if (this->load_and_apply_noise_psk_() && this->noise_ctx_.has_psk()) {
+8 -3
View File
@@ -43,6 +43,11 @@ struct SavedNoisePsk {
noise::psk_t psk;
} PACKED; // NOLINT
#endif
#if defined(USE_API_NOISE) && defined(USE_OTA_ENCRYPTION_PROVISIONED)
/// One-shot read of the provisioned key for a boot without an api server (safe mode); false when
/// there is no key
bool load_saved_noise_psk(noise::psk_t &out);
#endif
class APIServer final : public Component,
public Controller
@@ -314,7 +319,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 +356,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};
+7 -7
View File
@@ -181,11 +181,11 @@ async def at581x_settings_to_code(
await cg.register_parented(var, config[CONF_ID])
# Radar configuration
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
if (frontend_reset := config.get(CONF_HW_FRONTEND_RESET)) is not None:
template_ = await cg.templatable(frontend_reset, args, cg.int8)
cg.add(var.set_hw_frontend_reset(template_))
if freq := config.get(CONF_FREQUENCY):
if (freq := config.get(CONF_FREQUENCY)) is not None:
if not cg.is_template(freq):
freq = int(freq / 1000000)
template_ = await cg.templatable(freq, args, cg.int_)
@@ -195,19 +195,19 @@ async def at581x_settings_to_code(
template_ = await cg.templatable(sens_dist, args, cg.int_)
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
if (selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME)) is not None:
template_ = await cg.templatable(selfcheck, args, cg.int_)
cg.add(var.set_poweron_selfcheck_time(template_))
if protect := config.get(CONF_PROTECT_TIME):
if (protect := config.get(CONF_PROTECT_TIME)) is not None:
template_ = await cg.templatable(protect, args, cg.int_)
cg.add(var.set_protect_time(template_))
if trig_base := config.get(CONF_TRIGGER_BASE):
if (trig_base := config.get(CONF_TRIGGER_BASE)) is not None:
template_ = await cg.templatable(trig_base, args, cg.int_)
cg.add(var.set_trigger_base(template_))
if trig_keep := config.get(CONF_TRIGGER_KEEP):
if (trig_keep := config.get(CONF_TRIGGER_KEEP)) is not None:
template_ = await cg.templatable(trig_keep, args, cg.int_)
cg.add(var.set_trigger_keep(template_))
@@ -215,7 +215,7 @@ async def at581x_settings_to_code(
template_ = await cg.templatable(stage_gain, args, cg.int_)
cg.add(var.set_stage_gain(template_))
if power := config.get(CONF_POWER_CONSUMPTION):
if (power := config.get(CONF_POWER_CONSUMPTION)) is not None:
if not cg.is_template(power):
power = int(power * 1000000)
template_ = await cg.templatable(power, args, cg.int_)
+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
+4 -1
View File
@@ -3,6 +3,7 @@
#include <utility>
#include <numbers>
#include "display_color_utils.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -770,10 +771,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;
}
+19
View File
@@ -758,6 +758,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 +781,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 +811,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
-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);
+64 -1
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"
@@ -261,7 +262,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"esp_phy", # RF PHY - re-included by internal_temperature on the original ESP32; esp_wifi/bt/ieee802154 pull it back
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow, esp32_hosted; bt pulls it back for BLE builds
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"ieee802154", # 802.15.4 radio - IDF openthread and the Zigbee libs pull it back
@@ -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
@@ -2739,6 +2792,11 @@ async def to_code(config):
config.get(CONF_ENGINEERING_SAMPLE, False),
)
# ESP32-C2 defaults to the ROM's newlib "nano" printf, which does not
# understand %zu or %lld and crashes on any %s that follows one.
if variant == VARIANT_ESP32C2:
add_idf_sdkconfig_option("CONFIG_LIBC_NEWLIB_NANO_FORMAT", False)
# Set minimum chip revision for ESP32 variant
# Setting this to 3.0 or higher reduces flash size by excluding workaround code,
# and for PSRAM users saves significant IRAM by keeping C library functions in ROM.
@@ -2749,6 +2807,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);
@@ -290,6 +290,8 @@ async def to_code(config: ConfigType) -> None:
# symbols are simply unused and never register a callback at runtime.
if esp32.get_esp32_variant() == esp32.VARIANT_ESP32P4:
add_define("USE_ESP_NOW_HOSTED")
# esp_now_hosted.cpp includes esp_now.h, which esp_wifi provides
esp32.include_builtin_idf_component("esp_wifi")
# esp-hosted's CustomRpc ("peer data transfer") path — off by default.
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_ENABLE_PEER_DATA_TRANSFER", True
@@ -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};
+17 -13
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
@@ -308,20 +316,16 @@ async def to_code(config: ConfigType) -> None:
# One key per device: an api encryption block supplies it (static or
# runtime) and offers; the ota block only adds the requirement
api_conf = CORE.config.get(CONF_API) or {}
encryption_conf = config.get(CONF_ENCRYPTION)
own_key = None
if encryption_conf is not None and static_encryption_key(api_conf) is None:
own_key = encryption_conf[CONF_KEY]
if own_key is not None:
if key := static_encryption_key(config) or static_encryption_key(api_conf):
# Build time key: the ota keeps its own pointer so safe mode, which
# has no api server, still has it
cg.add_define("USE_OTA_ENCRYPTION")
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], own_key)))
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key)))
elif CONF_ENCRYPTION in api_conf:
# Runtime key: found in the api server, or in preferences in safe mode
cg.add_define("USE_OTA_ENCRYPTION")
cg.add_define("USE_OTA_ENCRYPTION_FROM_API")
if static_encryption_key(api_conf) is None:
# The key arrives at runtime, so the offer has to look for it
cg.add_define("USE_OTA_ENCRYPTION_PROVISIONED")
if encryption_conf is not None:
cg.add_define("USE_OTA_ENCRYPTION_PROVISIONED")
if CONF_ENCRYPTION in config:
cg.add_define("USE_OTA_ENCRYPTION_REQUIRED")
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
+17 -5
View File
@@ -1,5 +1,5 @@
#include "ota_esphome.h"
#ifdef USE_OTA_ENCRYPTION_FROM_API
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
#include "esphome/components/api/api_server.h"
#endif
#ifdef USE_OTA
@@ -32,11 +32,13 @@ static const char *const TAG = "esphome.ota";
#ifdef USE_OTA_ENCRYPTION
const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const {
#ifdef USE_OTA_ENCRYPTION_FROM_API
return api::global_api_server->get_noise_ctx();
#else
return this->noise_ctx_;
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
// The api server holds the live key; safe mode never constructs it, and then
// noise_ctx_ holds the saved key setup() found, if any
if (api::global_api_server != nullptr)
return api::global_api_server->get_noise_ctx();
#endif
return this->noise_ctx_;
}
#endif
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
@@ -58,6 +60,16 @@ extern "C" void esphome_wake_ota_component_any_context() {
}
void ESPHomeOTAComponent::setup() {
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
// Safe mode never constructs the api server, so read the key it saved
noise::psk_t psk;
if (api::global_api_server == nullptr && api::load_saved_noise_psk(psk)) {
this->saved_psk_ = RAMAllocator<noise::psk_t>().make_unique(psk);
if (this->saved_psk_ != nullptr) {
this->noise_ctx_.set_psk(this->saved_psk_->data());
}
}
#endif
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->server_ == nullptr) {
this->server_failed_(LOG_STR("creation"));
+6 -3
View File
@@ -44,7 +44,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
}
#endif // USE_OTA_PASSWORD
#if defined(USE_OTA_ENCRYPTION) && !defined(USE_OTA_ENCRYPTION_FROM_API)
#ifdef USE_OTA_ENCRYPTION
/// psk points at 32 bytes that live in flash for the life of the program
void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); }
#endif
@@ -86,7 +86,8 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
bool writing{false}; // a produced handshake frame is still being flushed
uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE];
};
// The api server's live context when the api has encryption, else our own
// The api server's live context when it exists, otherwise our own (a build
// time key, or the saved key loaded in safe mode)
const noise::NoiseContext &noise_context_() const;
bool noise_start_session_(uint8_t server_feature_flags);
bool handle_noise_handshake_();
@@ -148,8 +149,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
RAMUniquePtr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
#ifndef USE_OTA_ENCRYPTION_FROM_API
noise::NoiseContext noise_ctx_;
#ifdef USE_OTA_ENCRYPTION_PROVISIONED
// Backs noise_ctx_ in safe mode, where no api server holds the saved key
RAMUniquePtr<noise::psk_t> saved_psk_;
#endif
RAMUniquePtr<NoiseSession> noise_;
#endif // USE_OTA_ENCRYPTION
@@ -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)))
+5 -7
View File
@@ -48,14 +48,12 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
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);
case 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);
break;
}
default:
break;
}
-1
View File
@@ -54,7 +54,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
Loaded 100 of 389 files, more files were not shown because too many files have changed in this diff. Show more