Compare commits

...
Author SHA1 Message Date
J. Nick Koston 92f9a97869 Keep the state waiter apart from the states dict 2026-10-21 10:54:58 -10:00
J. Nick Koston 90ecc02630 Explain the backing timeout, drop a repeated comment and name the line waiter consistently 2026-10-21 10:52:49 -10:00
J. Nick Koston 73b08d732c [tests] Use LineWaiter for the integration tests that only wait on log lines 2026-10-21 10:28:59 -10:00
J. Nick Koston 63b610bf17 [tests] Wait for the ld2410 query ACK log line instead of asserting it arrived (#21191) 2026-10-21 10:13:22 -10:00
J. Nick Koston 62c9e05541 [tests] Wait for the template sensor's first reading before the Home Assistant service tests (#21302) 2026-10-21 09:56:20 -10:00
esphome[bot] 671ac3e0bc Bump bundled esphome-device-builder to 1.26.0 (#21299) 2026-10-21 09:45:18 -10:00
dependabot[bot] 6671ded245 Bump platformdirs from 4.13.3 to 4.13.4 (#21297)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-21 19:32:36 +00:00
dependabot[bot] 9010c4bb2e Bump filelock from 4.2.1 to 4.2.2 (#21295)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-21 19:32:16 +00:00
J. Nick Koston e0a48ff683 [waveshare_epaper] Set removal of the deprecated component for 2027.5.0 (#20692) 2026-10-21 19:06:38 +00:00
e3d7b957a7 [mqtt] increase TLS options and compatibility (#14318)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-21 19:02:31 +00:00
J. Nick Koston 9ab230f98d [speaker] Build the pipeline task names on the stack (#21203) 2026-10-21 08:41:59 -10:00
Jonathan Swoboda 35883270fe [ci] Ignore BUILD_ALWAYS external project byproducts in the idf.py check (#21291) 2026-10-21 08:35:58 -10:00
J. Nick Koston da37e89b9d [tests] Let the offline probe finish before checking that the continuous modbus poll stopped (#21293) 2026-10-21 08:35:38 -10:00
J. Nick Koston 32bbf55f2a [tests] Clear testing_mode after every test (#21292) 2026-10-21 08:35:22 -10:00
Kevin Ahrendt 16759c848f [i2s_audio] Convert audio to the configured slot width on the original ESP32 (#20893) 2026-10-21 14:25:38 -04:00
J. Nick Koston 244e419f82 [tests] Drop the timeout wrappers around the integration state helpers (#21245) 2026-10-21 08:04:00 -10:00
J. Nick Koston 04f53f0ab4 [tests] Fail the test when the integration state helpers time out (#21244) 2026-10-21 08:03:59 -10:00
9eef601f99 [network][socket] Allow disabling IPV4 (#14526)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-21 13:57:49 -04:00
J. Nick Koston 454dbe59db [tests] Speed up the modbus uart_mock integration tests (#21250) 2026-10-21 07:48:38 -10:00
J. Nick Koston 7c0fa49b00 [tests] Speed up the host OTA integration tests (#21248) 2026-10-21 13:29:58 -04:00
J. Nick KostonandJonathan Swoboda c9fe7451a6 [tests] Use wait_or_fail in the scheduler and loop integration tests (#21241)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-21 17:29:44 +00:00
J. Nick Koston 92320ed14e [tests] Use wait_or_fail in the uart_mock integration tests (#21247) 2026-10-21 13:28:32 -04:00
J. Nick Koston 601dcce0e4 [tests] Use require_entity and require_only_entity for the remaining entity lookups (#21243) 2026-10-21 13:20:50 -04:00
J. Nick Koston 980f415c64 [tests] Use require_service and assert_device_name in the online_image and backpressure tests (#21240) 2026-10-21 13:19:35 -04:00
J. Nick Koston 054071171c [tests] Use assert_device_name in the remaining object_id tests (#21238) 2026-10-21 13:19:16 -04:00
J. Nick Koston a8ab7adf0d [nfc] Stop copying the binary sensor match string (#21208) 2026-10-21 13:14:28 -04:00
J. Nick Koston 3bf98fbc98 [mqtt_subscribe] Stop copying the topic (#21207) 2026-10-21 13:10:01 -04:00
J. Nick Koston 8906a3e1bf [tuya] Store the product text in an exact-size buffer (#21206) 2026-10-21 13:09:41 -04:00
J. Nick Koston 0dd02fd68a [thermopro_ble] Pick the parser from the name without storing it (#21205) 2026-10-21 13:08:24 -04:00
J. Nick Koston 1fa828bf9e [gps] Size the listener list at compile time (#21201) 2026-10-21 13:05:34 -04:00
J. Nick Koston 55a658dca6 [slow_pwm] Group the bool members to drop padding (#21200) 2026-10-21 13:02:16 -04:00
J. Nick Koston 436d756396 [ina2xx_base] Expand adc_time in the schema (#21197) 2026-10-21 13:00:55 -04:00
J. Nick Koston 8225fd95e7 [ina226] Expand adc_time in the schema (#21196) 2026-10-21 13:00:29 -04:00
J. Nick KostonandJonathan Swoboda 3cd3d776e7 [climate] Keep custom fan modes and presets in a ConstVector; remove the deprecated traits setters (#20213)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-21 12:42:49 -04:00
J. Nick Koston f3c025c6e3 [fan] Keep preset modes in a ConstVector; remove the deprecated traits setters (#20214) 2026-10-21 12:24:35 -04:00
Jonathan Swoboda 7784210ec4 [epaper_spi] List the SSD1681 driver source on its model (#21270) 2026-10-21 08:42:01 -04:00
Bonne EgglestonandJ. Nick Koston e4dd6d452d [epaper_spi] Add SSD1681 model with differential partial refresh (#20332)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-21 08:18:32 -04:00
Jonathan Swoboda 3f29f1a5f1 [esp32_ulp][esp32] Add component to run programs on the ULP coprocessor (#21127) 2026-10-21 08:16:36 -04:00
Jonathan Swobodaandpre-commit-ci-lite[bot] 80fedf020f [ci] Bump clang-format from 13.0.1 to 22.1.8 (#20551)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-21 08:15:55 -04:00
Jesse Hills 3a15ffa8df Merge branch 'release' into dev 2026-10-22 00:43:07 +13:00
Jesse Hills 704551a6c9 Merge pull request #21268 from esphome/bump-2026.10.0
2026.10.0
2026-10-22 00:42:40 +13:00
Jesse Hills f30e70acfa Bump version to 2026.10.0 2026-10-21 23:53:12 +13:00
Jeroen 594a691d9f [web_server_idf] Increase HTTP server task stack to 4512 bytes (#21038) 2026-10-21 23:53:09 +13:00
Hubert Teo ab41553e8d [beken_spi_led_strip] Fix LED strip component for bk7231t (#21259) 2026-10-20 22:31:22 -10:00
Bryce Boe 82a0c5b2f5 [sx126x][sx127x] Move CONF_BITRATE to the shared component constants (#21261) 2026-10-20 22:13:35 -10:00
Jeroen 659eab1958 [web_server_idf] Increase HTTP server task stack to 4512 bytes (#21038) 2026-10-20 19:59:52 -10:00
Jesse Hills fb2ed65d1b Merge branch 'beta' into dev 2026-10-21 16:40:13 +13:00
Jesse Hills 6c90b5c549 Merge pull request #21254 from esphome/bump-2026.10.0b6
2026.10.0b6
2026-10-21 16:39:54 +13:00
Pat Satyshur 940de19d3b [pca9554] Add a device option with the PCA953x/PCA955x/TCA955x names and the PCAL9538 and PCAL9555 expanders (#15449) 2026-10-21 03:26:25 +00:00
Jesse Hills c8df23a9cb Bump version to 2026.10.0b6 2026-10-21 16:17:51 +13:00
4fdce9d96b [web_server_idf] Reduce loopTask stack usage for state SSE (#21109)
Co-authored-by: Jeroen Jansen <jeroen@Jeroens-MacBook-Air.local>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-21 16:17:49 +13:00
J. Nick Koston 9d296a5748 [ota] Accept an unknown image size in the RP2040 backend so web_server OTA works (#21178) 2026-10-21 16:17:49 +13:00
esphome[bot] f2e013d71a Bump bundled esphome-device-builder to 1.25.0 (#21175) 2026-10-21 16:17:49 +13:00
mrtoy-me ded94c6698 [sx126x][sx127x] Move CONF_MODULATION to the shared component constants (#21249) 2026-10-20 16:50:49 -10:00
Jesse Hills e415c099e0 [font] Crop blank space from glyph bitmaps (#21235) 2026-10-21 15:46:15 +13:00
Jesse Hills d25a925a9b [epaper_spi] Only compile the driver sources the configured models need (#21228) 2026-10-21 15:39:52 +13:00
will-tm 41c3bbff25 [vl53l1x] Add VL53L1X time-of-flight distance sensor (#13605) 2026-10-20 16:05:46 -10:00
J. Nick Koston 3b8b6c6429 [ac_dimmer] Pack members and drop the unused zero-cross ISR pin (#21193) 2026-10-20 15:51:07 -10:00
J. Nick Koston 7c58a9698b [tests] Use require_service in the integration tests (#21214) 2026-10-21 01:47:31 +00:00
J. Nick Koston f47dca7e4e [tests] Use the exact entity lookups in the integration tests (#21213) 2026-10-20 15:42:31 -10:00
J. Nick Koston 701ba8eaa5 [key_collector] Stop copying the key strings (#21210) 2026-10-20 15:42:05 -10:00
J. Nick Koston 7af7b6f1d6 [bme680_bsec] Stop copying the device id (#21209) 2026-10-20 15:34:28 -10:00
J. Nick Koston 7fe4352786 [graph] Stop copying the trace name (#21211) 2026-10-20 15:33:38 -10:00
J. Nick Koston e70b0ace13 [display_menu_base] Stop copying the number format and switch texts (#21212) 2026-10-20 15:33:12 -10:00
J. Nick Koston 915458cfcf [tests] Use assert_device_name in the a-l integration tests (#21215) 2026-10-20 15:32:07 -10:00
J. Nick Koston 4eaad21f08 [tests] Use assert_device_name in the m-z integration tests (#21216) 2026-10-20 15:31:14 -10:00
J. Nick Koston 6e4e5ae830 [mqtt] Print the dump_config header in LOG_MQTT_COMPONENT (#21226) 2026-10-20 15:30:18 -10:00
J. Nick Koston a66faec424 [ci] Add the speaker media player, web_server_idf and MQTT WebSocket flows to the hardware-test skill (#21229) 2026-10-20 15:29:45 -10:00
J. Nick Koston d24cadbc8f [components] Use cv.only_when and cv.key_requires for key dependencies (#21236) 2026-10-20 15:27:03 -10:00
J. Nick Koston f9373bd7b3 [core] Add cv.only_when and cv.key_requires key validators (#21234) 2026-10-20 15:27:03 -10:00
Remco van Essen eb32f80d53 [speaker][speaker_source] Add a set volume for announcements that start while nothing is playing (#21002) 2026-10-20 14:48:11 -10:00
cef13c8697 [mqtt] Add MQTT over WebSocket transport for ESP32 IDF (#16056)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-20 23:33:06 +00:00
epicRE 876537186c [apc1][systa_bus] Move CONF_ERROR_CODE to the shared component constants (#21230) 2026-10-20 11:16:19 -10:00
Jacopo Labardi 6bb51c8cd9 [vbus] Discard P1 telegrams after a frame checksum error (#21045) 2026-10-20 11:15:40 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] 5d9ca0e45b [core] Share one log_entity_header() for entity dump_config headers (#21116)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-20 20:53:28 +00:00
f3f056ca41 [gree] Add sleep preset support for GREE YAC1FB9 (#18090)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-20 10:42:40 -10:00
bf36e7c33a [web_server_idf] Reduce loopTask stack usage for state SSE (#21109)
Co-authored-by: Jeroen Jansen <jeroen@Jeroens-MacBook-Air.local>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-20 10:41:10 -10:00
dependabot[bot] bac4d2aa29 Bump yamlrocks from 0.7.0 to 0.7.1 (#21223)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-20 19:32:37 +00:00
dependabot[bot] 992860637b Bump pillow from 12.3.0 to 13.0.0 (#21224)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-20 09:32:19 -10:00
dependabot[bot] b0d3899483 Bump pytest-mock from 3.16.0 to 3.17.0 (#21225)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-20 19:32:15 +00:00
dependabot[bot] f46ebdd345 Bump filelock from 4.2.0 to 4.2.1 (#21222)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-20 09:31:45 -10:00
fbe4699ad4 [nextion] Fix: Properly deliver numeric responses (#17744)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:08:41 -10:00
Jesse Hills 6423af2978 [core] Replace flake8 with ruff docstring rules and drop unused scripts (#21176) 2026-10-21 07:32:56 +13:00
J. Nick Koston ebc9619e3b [sx126x] Pass the sx1261 hardware version as a flag (#21202) 2026-10-20 11:36:38 -04:00
663 changed files with 10032 additions and 5384 deletions
+7 -2
View File
@@ -115,7 +115,7 @@ Per-topic scripts, each described in the matching `reference/recipes/<component>
(host driver, Zephyr driver probe), `rtsp_server/` (protocol client, raw rate reader, measurement set,
API image grab), `cse7766/` (API observer with ping), `zephyr_coredump/` (reader flow, decode with the
Zephyr tools), `libretiny_prefs/` (rtc preference harness header), `libretiny_crash/` (BK72xx crash probe header and
capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player).
capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player), `speaker_media_player/` (API player driver, pipeline task case runner), `web_server/` (SSE client, request mix, SSE and HTTPD case runners), `mqtt/` (broker config with WebSocket listeners, test CA, transport case runner).
## Templates in `$SKILL/templates`
@@ -131,7 +131,8 @@ capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `l
`homeassistant-light-dut.yaml`, `deep-sleep-outgoing-dut.yaml`, `outgoing-wifi-drop-dut.yaml`,
`globals-storage-dut.yaml`, `usb-audio-waveshare-s3-eth-dut.yaml`, `usb-audio-sendspin-s3-eth-dut.yaml`,
`cast-pro-1-sendspin-control.yaml`, `aux-2-preprod-stock-dut.yaml`, `aux-2-preprod-usb-audio-dut.yaml`,
`dfrobot-sen0395-dut.yaml`.
`dfrobot-sen0395-dut.yaml`, `aux-2-preprod-speaker-player-dut.yaml`, `web-server-sse-dut.yaml`,
`mqtt-transport-dut.yaml`.
## When something looks wrong
@@ -145,6 +146,10 @@ capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `l
preference and repeat it in a periodic alive line, as `scripts/libretiny_prefs/ltprefs.hpp` does.
- Multicast from the host never arrives on some access points: test it device to device. Broadcast
is lossy under Wi-Fi power save, unicast is not. Throttle UDP floods to a fixed packet rate.
- The merge base comes from a freshly fetched `origin/dev`; a stale local `dev` puts dependency bumps into the
PR diff and builds the control from the wrong commit.
- A `.local` name can stop resolving in the middle of a run; keep the address from the boot log and drive by it.
- `wc -l` pads its count on macOS, so `tail -n +$n0` needs `$((n0+0))` or an arithmetic expansion.
- `api:` reboots a board after 15 min without a client: `reboot_timeout: 0s` on `api:` and `mqtt:` for
soak tests.
@@ -11,7 +11,7 @@ One file per component, named after it:
- [`hdmi_cec.md`](hdmi_cec.md): two-board wire
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx crash report on the next boot
- [`mqtt.md`](mqtt.md): broker bench, QoS, reconnect, crash log over MQTT
- [`mqtt.md`](mqtt.md): broker bench, QoS, reconnect, crash log over MQTT, WebSocket transports with a local and a public broker
- [`nrf52.md`](nrf52.md): dongle board support
- [`online_image.md`](online_image.md): stalled and malformed downloads
- [`openthread.md`](openthread.md): border router on the S3 host plus H2 RCP
@@ -28,3 +28,5 @@ One file per component, named after it:
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant, held reports versus later calls
- [`usb_audio.md`](usb_audio.md): USB audio speaker and microphone, the usb_host FIFO split, and Music Assistant playback through sendspin
- [`dfrobot_sen0395.md`](dfrobot_sen0395.md): SEN0395 mmWave command flow, module quirks and overlong lines through its error reply
- [`speaker.md`](speaker.md): media player pipeline tasks on the Connect AUX-2 with the `speaker` platform player
- [`web_server.md`](web_server.md): state SSE stress and the HTTPD task stack on web_server_idf
@@ -10,3 +10,22 @@ after bulk subscribe and unsubscribe; read the heap seconds later over several r
leak. Spawned subscriptions need about 2.5 s to reach the broker before the next publish.
`esphome logs --device MQTT` only uses the MQTT transport when the config has no `api:`. Driver pattern: a
paho script publishes and polls the serial capture file.
## WebSocket transports on ESP-IDF (#16056)
One build per transport from `templates/mqtt-transport-dut.yaml`, with `scripts/mqtt/mosquitto-ws.conf` as the
local broker (mosquitto 2.x from brew has `websockets` support; `log_type all` prints `New connection from <ip>:<port>
on port <listener>`, which proves which listener each client used) and `scripts/mqtt/make_test_ca.sh BROKER_IP` for
the TLS listener's certificate, both run in the scratch directory so keys, logs and outputs stay out of the checkout. `scripts/mqtt/cases.sh NAME KEY BROKER_HOST SUB_PORT CAPTURE [DEV_HOST] [restart]` subscribes
to `NAME/#`, sends a switch command and an `on_message` topic, presses the 3000 byte publish button over the API,
restarts the local broker when the last argument is `restart` (the PID in `mosquitto.pid`, written when the broker
was started with `mosquitto -c mosquitto-ws.conf & echo $! > mosquitto.pid` in the working directory next to the
certificates), checks its three ports answer again, and prints the broker's message counts with the device log. `mosquitto_sub` cannot speak
WebSocket, so it subscribes on the plain listener of the same broker. Cases: `tcp` on 1883, `ws` on 9001, `wss` on
9002 with `certificate_authority` (the certificate names an address, so `skip_cert_cn_check: true`), `wss` to
test.mosquitto.org on 8081 with no CA for the certificate bundle, and two negatives that must never connect: a
different CA (`mbedtls_ssl_handshake returned -0x2700`) and the self-signed server through the bundle
(`esp-x509-crt-bundle: Failed to verify certificate`, `-0x3000`). `CONFIG_MQTT_TRANSPORT_WEBSOCKET` is on by
default in the generated sdkconfig. The TLS session costs about 29 kB of internal heap while connected; the public
broker carries other devices' `esphome/discover` traffic, which is noise. Results:
https://github.com/esphome/esphome/pull/16056#issuecomment-6301034019
@@ -0,0 +1,19 @@
# speaker
## Media player pipeline tasks (#21203)
The `speaker` media player platform (`speaker/media_player/`) creates `ann_read`, `ann_decode`, `med_read` and
`med_decode` FreeRTOS tasks on every play and deletes them after a stop. The AUX-2 and sendspin packages use the
`speaker_source` player instead, which has its own pipeline and never runs that code, so a test of
`speaker/media_player/*` has to swap the player: `templates/aux-2-preprod-speaker-player-dut.yaml` removes the
package's `external_media_player` with `id: !remove` and adds a `speaker` platform player on the package's resampler
speakers, plus a "Task names" button whose lambda calls `xTaskGetHandle()` on the four names and logs internal free
heap. Serve FLAC files from the host (`ffmpeg -f lavfi -i "sine=frequency=440:duration=90" -ac 2 -ar 48000 -c:a flac
media90.flac`, a 3 s mono one for announcements, `python3 -m http.server 8765 --directory www`); `audio_http`
refuses WAV and MP3. `scripts/speaker_media_player/cases.sh HOST KEY MEDIA_URL ANN_URL CAPTURE LABEL` runs three
cycles through `drive.py` (`play:URL | ann:URL | stop | press:NAME | wait:S` on one API connection) with the probe
pressed idle, during media, during the announcement, after it and after the stop. Pass: idle none, media `med_*`
only, announcement all four, after stop none, every cycle. API commands land a second or two late over Wi-Fi, so read
the probe against the `State changed to` lines rather than the driver's timestamps. Internal free heap after a stop
moved by 2.5 kB between two boots of the same build, so a heap delta needs more than one boot per build. Results:
https://github.com/esphome/esphome/pull/21203#issuecomment-6299581943
@@ -0,0 +1,31 @@
# web_server
## State SSE on web_server_idf (#21109) and the HTTPD task stack (#21038)
`templates/web-server-sse-dut.yaml` (ESP32 classic, ESP-IDF, `web_server` version 3) publishes eight sensors every
200 ms (small state events on the 1024 byte stack buffer) and three text sensors whose JSON carries the string twice
(`value` and `state`): 600 chars for about 1.3 kB (the 2048 byte tail), 1800 chars for about 3.7 kB (tail grown to
4096) and 2800 chars for about 5.7 kB, over the 5120 byte JSON room, which must log `State event does not fit`
and never put a truncated frame on the wire. An `interval` lambda logs `uxTaskGetStackHighWaterMark()` of the loop
task (`nullptr`) and of the HTTPD task (`xTaskGetHandle("httpd")`) every 5 s; the ESPHome loop task is `loopTask`
on ESP32. Keep `power_save_mode: none`, and drive the device by address: mDNS names dropped out mid-run once.
`scripts/web_server/sse.py HOST SECONDS [--clients N] [--stall] [--churn CYCLES]` opens raw TCP connections to
`/events`, decodes the chunked body and parses every `state` event as JSON; `--stall` adds a connection with a 2 kB
receive buffer that never reads (the device must log `Closing stuck EventSource connection` after the 20 s stall
timeout), `--churn` opens and resets a connection CYCLES times. `sse_cases.sh HOST KEY LABEL CAPTURE` runs the
set: 1 client 60 s, 4 clients 60 s, a reader plus a stalled client 45 s, a reader plus 10 abrupt closes, then two
clients with the "Burst" button (50 sensor and 50 text publishes in one loop pass). Pass: 0 invalid JSON on every
client, the oversize sensor dropped with its warning on every publish, the stalled session closed after about
20 s, no reset. The loop task watermark is a minimum since boot, so compare the value after the first client, after
four clients and after the burst between the builds (the SSE split in #21109 left 1280 B more on the ESP32 classic).
For the HTTPD task, `web_server: auth: type: digest` (the default `auth` is Basic) and
`scripts/web_server/httpd_cases.sh HOST USER PASS LABEL CAPTURE`: `httpmix.sh` sends a `curl --anyauth` request
mix per round (page, `/sensor/<Name>`, `/text_sensor/<Name>`, `/switch/<Name>`, a `POST` with `Content-Length: 0`
to `/switch/<Name>/toggle`, an unknown path, which the IDF server closes without a response, an unauthenticated
request answered 401, and a 2 s `/events` stream), 20 rounds alone and 12 rounds next to two `ssecurl.sh` SSE
clients. Entity URLs use the entity name, URL encoded, not an object id. The HTTPD watermark follows the deepest
request of the boot; compare after the mix and after the SSE round, with two boots when the numbers are close.
Results: https://github.com/esphome/esphome/pull/21109#issuecomment-6300582328 and
https://github.com/esphome/esphome/pull/21038#issuecomment-6301310182
@@ -1,6 +1,6 @@
"""Press a template button on the test device over the API, or list its buttons.
usage: api_button.py HOST KEY list | press "Button name" [...]
usage: api_button.py HOST KEY list | press "Button name" [...] (KEY of - means a plaintext API)
"""
import asyncio
@@ -13,7 +13,7 @@ CMD, ARGS = sys.argv[3], sys.argv[4:]
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=KEY)
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
ents, _ = await c.list_entities_services()
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
# cases.sh NAME KEY BROKER_HOST SUB_PORT CAPTURE [DEV_HOST] [restart] : subscribe to NAME/# on the broker, send a
# switch command and an on_message topic, press the 3000 byte publish button over the API (KEY, - for plaintext),
# optionally restart the local broker (its PID from mosquitto.pid in the working directory, written when it was started
# with `mosquitto -c mosquitto-ws.conf & echo $! > mosquitto.pid`), and show the device log. Outputs (sub-NAME.txt, mosquitto.out) go to the working directory, which should be the scratch directory.
N=$1; KEY=$2; B=$3; SP=$4; LOG=$5; DEV=${6:-$1.local}; RESTART=$7; HERE=$(cd "$(dirname "$0")" && pwd); PY=${PYTHON:-python3}; WORK=$PWD
BTN=$HERE/../api_button.py
[ -n "$KEY" ] && [ -r "$LOG" ] || { echo "usage: cases.sh NAME KEY BROKER_HOST SUB_PORT CAPTURE [DEV_HOST] [restart] (CAPTURE must be a readable file)"; exit 2; }
show() { tail -n +$(($1+1)) "$LOG" | sed 's/\x1b\[[0-9;]*m//g' | grep -a 'mqtt\|\[probe:\|cmd payload\|Relay\|\[W\]\|\[E\]\|Guru\|abort\|Backtrace\|ESPHome version' | grep -a -v 'took a long time\|Publish.*Counter\|sensor/counter' | cut -c1-170; }
pub() { mosquitto_pub -h "$B" -p "$SP" -t "$1" -m "$2" || { echo "mosquitto_pub to $1 failed"; exit 1; }; }
n0=$(($(wc -l < "$LOG")+0))
timeout 75 mosquitto_sub -h "$B" -p "$SP" -v -t "$N/#" > "$WORK/sub-$N.txt" 2> "$WORK/sub-$N.err" &
sub_pid=$!
sleep 20
echo "=== $N: command to the switch and the 3000 byte publish ($(date +%T))"
pub "$N/switch/relay/command" ON; sleep 2
pub "$N/cmd" "hello-over-$N"; sleep 2
$PY "$BTN" "$DEV" "$KEY" press "Publish big" || { echo "button press failed"; exit 1; }; sleep 3
pub "$N/switch/relay/command" OFF; sleep 2
if [ "$RESTART" = restart ]; then
[ -f "$WORK/mosquitto-ws.conf" ] || { echo "no mosquitto-ws.conf in $WORK, cannot restart the broker"; exit 1; }
echo "=== $N: broker restart ($(date +%T))"; n1=$(($(wc -l < "$LOG")+0))
# Only this run's broker is signalled: its PID comes from mosquitto.pid, written when it was started
pid=$(cat "$WORK/mosquitto.pid" 2>/dev/null); [ -n "$pid" ] || { echo "no mosquitto.pid in $WORK, cannot restart the broker"; exit 1; }
kill "$pid" || { echo "broker PID $pid is not running, nothing to restart"; exit 1; }; sleep 8
nc -z -w 3 "$B" 1883 && { echo "the old broker still answers on 1883 after the kill"; exit 1; }
(cd "$WORK" && mosquitto -c mosquitto-ws.conf > mosquitto.out 2>&1 & echo $! > "$WORK/mosquitto.pid"); sleep 25
for port in 1883 9001 9002; do nc -z -w 3 "$B" "$port" || { echo "broker port $port not listening after the restart"; exit 1; }; done
tail -n +$((n1+1)) "$LOG" | grep -a -q "MQTT_EVENT_CONNECTED\|\[mqtt:[0-9]*\]: Connected" || { echo "the device did not reconnect after the broker restart"; exit 1; }
else
echo "=== $N: no broker restart (pass 'restart' as the 7th argument for a local broker)"
fi
wait $sub_pid; sub_rc=$?
[ "$sub_rc" = 124 ] || { echo "subscriber ended with status $sub_rc: $(cat "$WORK/sub-$N.err")"; exit 1; }
echo "--- broker saw ($(grep -c . "$WORK/sub-$N.txt") messages): $(cut -c1-60 "$WORK/sub-$N.txt" | awk '{print $1}' | sort | uniq -c | tr '\n' ';')"
big=$(grep "^$N/big " "$WORK/sub-$N.txt" | awk '{print length($2)}' | tr '\n' ' ')
echo "--- big payload bytes seen: $big"
echo "--- device log:"; show "$n0"
case " $big " in *" 3000 "*) ;; *) echo "the 3000 byte payload did not reach the broker"; exit 1;; esac
for want in "Relay ON" "cmd payload: hello-over-$N" "Relay OFF"; do
tail -n +$((n0+1)) "$LOG" | grep -a -q "$want" || { echo "the device never logged '$want': the broker to device path failed"; exit 1; }
done
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# make_test_ca.sh BROKER_IP : a throwaway CA and a server certificate for the broker address (30 days), written into
# the working directory; run it in the scratch directory where a copy of mosquitto-ws.conf lives. The device
# config takes ca.crt as certificate_authority (skip_cert_cn_check: true, the name check does not match an address);
# other-ca.crt is a second CA for the negative case.
set -e
IP=$1
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.crt -days 30 -subj "/CN=hwtest-ca"
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=hwtest-broker"
printf "subjectAltName=IP:%s\nextendedKeyUsage=serverAuth\n" "$IP" > san.ext
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 30 -extfile san.ext
openssl req -x509 -newkey rsa:2048 -nodes -keyout other-ca.key -out other-ca.crt -days 30 -subj "/CN=other-ca"
openssl verify -CAfile ca.crt server.crt
@@ -0,0 +1,14 @@
# Copy next to the certificates in the scratch directory and start it there with
# `mosquitto -c mosquitto-ws.conf & echo $! > mosquitto.pid` (cases.sh restarts that PID): plain MQTT, MQTT over WebSocket, and MQTT over WebSocket with TLS (server.crt and
# server.key from make_test_ca.sh in the same directory). log_type all gives the listener port per connection.
listener 1883
protocol mqtt
listener 9001
protocol websockets
listener 9002
protocol websockets
certfile server.crt
keyfile server.key
allow_anonymous true
log_type all
log_dest file mosquitto.log
@@ -0,0 +1,12 @@
#!/bin/sh
# cases.sh HOST KEY MEDIA_URL ANN_URL CAPTURE LABEL : three play, announce, stop cycles on a speaker media player with the
# "Task names" probe button pressed idle, during media, during the announcement, after it and after the stop.
# KEY of - is plaintext. Serve FLAC files with `python3 -m http.server 8765 --directory www` (audio_http refuses WAV and MP3).
H=$1; K=$2; MEDIA=$3; ANN=$4; LOG=$5; LABEL=$6; PY=${PYTHON:-python3}; HERE=$(dirname "$0")
[ -r "$LOG" ] || { echo "usage: cases.sh HOST KEY MEDIA_URL ANN_URL CAPTURE LABEL (CAPTURE must be a readable file)"; exit 2; }
show() { tail -n +$(($1+1)) "$LOG" | sed 's/\x1b\[[0-9;]*m//g' | grep -a '\[tasks:\|speaker_media_player\|speaker_mixer\|\[W\]\|\[E\]\|Guru\|abort\|Backtrace' | grep -a -v 'took a long time' | cut -c1-180; }
for cycle in 1 2 3; do
n0=$(wc -l < "$LOG"); echo "=== $LABEL cycle $cycle ($(date +%T))"
$PY "$HERE/drive.py" "$H" "$K" "press:Task names" wait:1 "play:$MEDIA" wait:5 "press:Task names" wait:1 "ann:$ANN" wait:1.5 "press:Task names" wait:5 "press:Task names" wait:1 stop wait:3 "press:Task names" || { rc=$?; echo "driver failed ($rc), aborting"; exit $rc; }
sleep 1; echo "--- device log:"; show "$n0"
done
@@ -0,0 +1,50 @@
"""Drive the speaker platform media player (the first media player found) over one API connection.
usage: drive.py HOST KEY op [op ...]
ops: play:URL | ann:URL | stop | press:BUTTON | wait:S
"""
import asyncio
import sys
from aioesphomeapi import APIClient, ButtonInfo, MediaPlayerCommand, MediaPlayerInfo
HOST, KEY, OPS = sys.argv[1], sys.argv[2], sys.argv[3:]
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
try:
await run_ops(c)
finally:
await c.disconnect()
async def run_ops(c: APIClient) -> None:
ents, _ = await c.list_entities_services()
player = next((e for e in ents if isinstance(e, MediaPlayerInfo)), None)
if player is None:
sys.exit("no media player on this device")
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
for op in OPS:
kind, _, rest = op.partition(":")
print(f"{asyncio.get_running_loop().time():.1f} {op}", flush=True)
if kind == "play":
c.media_player_command(player.key, media_url=rest)
elif kind == "ann":
c.media_player_command(player.key, media_url=rest, announcement=True)
elif kind == "stop":
c.media_player_command(player.key, command=MediaPlayerCommand.STOP)
elif kind == "press":
if rest not in buttons:
sys.exit(f"no button {rest!r}; have {sorted(buttons)}")
c.button_command(buttons[rest].key)
elif kind == "wait":
await asyncio.sleep(float(rest))
else:
sys.exit(f"unknown op {op!r}")
await asyncio.sleep(0.5)
asyncio.run(main())
@@ -0,0 +1,17 @@
#!/bin/sh
# httpd_cases.sh HOST USER PASS LABEL CAPTURE : HTTPD task stack load for the web_server_idf task size; probe lines from
# CAPTURE, SSE client output into the working directory. A failed step ends the run with its status.
H=$1; U=$2; P=$3; LABEL=$4; LOG=$5; HERE=$(dirname "$0")
[ -r "$LOG" ] || { echo "usage: httpd_cases.sh HOST USER PASS LABEL CAPTURE (CAPTURE must be a readable file)"; exit 2; }
probes() { tail -n +$(($1+1)) "$LOG" | sed 's/\x1b\[[0-9;]*m//g' | grep -a -o "\[[0-9:]*\]\[I\]\[probe:[0-9]*\]: .*httpd min free stack [0-9]*\|\[[0-9:]*\]\[[WE]\]\[[a-z_.]*:[0-9]*\].*\|Guru.*" | grep -a -v "took a long time\|does not fit\|\]\[httpd\]: /" | sed 's/\[I\]\[probe:[0-9]*\]: loopTask min free stack \([0-9]*\), httpd min free stack \([0-9]*\)/ loop \1 httpd \2/' | uniq -f1 | cut -c1-140; }
step() { n0=$(wc -l < "$LOG"); echo "=== $LABEL: $1 ($(date +%T))"; shift; "$@" || { rc=$?; echo "step failed ($rc)"; exit $rc; }; echo "--- device log:"; probes "$n0"; }
step "idle 15 s" sleep 15
step "request mix, 20 rounds" sh "$HERE/httpmix.sh" "$H" "$U" "$P" 20
# Two SSE clients next to the request mix; arguments stay quoted, nothing is reparsed as shell code
concurrent() {
sh "$HERE/ssecurl.sh" "$H" "$U" "$P" 40 "$PWD/sse1-$LABEL.txt" & p1=$!
sh "$HERE/ssecurl.sh" "$H" "$U" "$P" 40 "$PWD/sse2-$LABEL.txt" & p2=$!
sh "$HERE/httpmix.sh" "$H" "$U" "$P" 12 && wait $p1 && wait $p2
}
step "2 SSE clients 40 s with the request mix alongside" concurrent
step "idle 15 s after" sleep 15
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
# httpmix.sh HOST USER PASS ROUNDS : authenticated request mix (curl --anyauth follows the server's Basic or Digest
# challenge): page, two json endpoints, a switch POST, a 404, an unauthenticated 401 and a 2 s SSE stream per round
H=$1; U=$2; P=$3; N=$4; ok=0; bad=0
# chk EXPECTED LABEL curl-args... : only LABEL is printed on a miss, never the argument list with the credentials
chk() { exp=$1; label=$2; shift 2; c=$(curl -s -m 6 -o /dev/null -w "%{http_code}" "$@"); if [ "$c" = "$exp" ]; then ok=$((ok+1)); else bad=$((bad+1)); echo "expected $exp got $c: $label"; fi; }
sse_tmp=$(mktemp)
for i in $(seq 1 $N); do
page=$(curl -s -m 6 -o /dev/null -w "%{http_code}" -u "$U:$P" --anyauth "http://$H/")
if [ "$page" = 200 ]; then ok=$((ok+1)); else bad=$((bad+1)); echo "expected 200 got $page: /"; fi
chk 200 "GET /sensor/Fast%201" -u "$U:$P" --anyauth "http://$H/sensor/Fast%201"
chk 200 "GET /text_sensor/Big" -u "$U:$P" --anyauth "http://$H/text_sensor/Big"
chk 200 "GET /switch/Toggle" -u "$U:$P" --anyauth "http://$H/switch/Toggle"
chk 200 "POST /switch/Toggle/toggle" -u "$U:$P" --anyauth -X POST -H "Content-Length: 0" "http://$H/switch/Toggle/toggle"
# The IDF server closes an unknown path without a response: curl ends with 52 (empty reply) or 56 (reset), never a
# timeout (28) or a refused connection (7); only trust the close when the page just answered
c=$(curl -s -m 6 -o /dev/null -w "%{http_code}" -u "$U:$P" --anyauth "http://$H/nonexistent"); rc=$?
if [ "$c" = 000 ] && [ "$page" = 200 ] && { [ "$rc" = 52 ] || [ "$rc" = 56 ]; }; then ok=$((ok+1)); else bad=$((bad+1)); echo "unknown path: got $c, curl status $rc, with the page at $page"; fi
chk 401 "GET / without credentials" "http://$H/"
timeout 2 curl -s -N -u "$U:$P" --anyauth -o "$sse_tmp" "http://$H/events" 2>/dev/null; rc=$?
if [ "$rc" = 124 ] && grep -q "^event: state" "$sse_tmp"; then ok=$((ok+1)); else bad=$((bad+1)); echo "SSE stream: curl status $rc, $(grep -c '^event: state' "$sse_tmp") state events"; fi
sleep 1
done
rm -f "$sse_tmp"
echo "httpmix: $ok ok, $bad unexpected"
[ "$bad" = 0 ]
@@ -0,0 +1,187 @@
"""SSE client(s) for a web_server /events stream: counts state events and checks each is valid JSON.
usage: sse.py HOST SECONDS [--clients N] [--stall] [--churn CYCLES]
--stall opens one extra connection that never reads (the device's socket buffer fills); --churn opens
and abruptly closes a connection CYCLES times, 2 s apart, next to the reading clients.
"""
import argparse
from collections import Counter
import contextlib
import json
import socket
import sys
import threading
import time
ap = argparse.ArgumentParser()
ap.add_argument("host")
ap.add_argument("seconds", type=float)
ap.add_argument("--clients", type=int, default=1)
ap.add_argument("--stall", action="store_true")
ap.add_argument("--churn", type=int, default=0)
args = ap.parse_args()
REQ = b"GET /events HTTP/1.1\r\nHost: %s\r\nAccept: text/event-stream\r\nConnection: keep-alive\r\n\r\n"
def read_chunked(sock, deadline, on_line):
"""Decode HTTP chunked transfer encoding and feed complete event-stream lines to on_line."""
buf = b""
body = b""
headers_done = False
sock.settimeout(1.0)
while time.monotonic() < deadline:
try:
data = sock.recv(65536)
except TimeoutError:
continue
if not data:
return "closed"
buf += data
if not headers_done:
i = buf.find(b"\r\n\r\n")
if i < 0:
continue
headers_done = True
buf = buf[i + 4 :]
while True:
i = buf.find(b"\r\n")
if i < 0:
break
size = int(buf[:i].split(b";")[0], 16)
if len(buf) < i + 2 + size + 2:
break
body += buf[i + 2 : i + 2 + size]
buf = buf[i + 2 + size + 2 :]
if size == 0:
return "closed"
while b"\n" in body:
line, body = body.split(b"\n", 1)
on_line(line.rstrip(b"\r"))
return "deadline"
class Client(threading.Thread):
def __init__(self, idx, seconds):
super().__init__(daemon=True)
self.idx = idx
self.seconds = seconds
self.events = Counter()
self.ids = Counter()
self.invalid = 0
self.max_data = 0
self.bytes = 0
self.end = "?"
self.cur_event = None
def on_line(self, line):
self.bytes += len(line) + 1
if line.startswith(b"event:"):
self.cur_event = line[6:].strip().decode()
elif line.startswith(b"data:"):
ev = self.cur_event or "message"
self.events[ev] += 1
data = line[5:].strip()
self.max_data = max(self.max_data, len(data))
if ev in ("state", "state_detail_all"):
try:
obj = json.loads(data)
self.ids[obj.get("id", "?")] += 1
except ValueError:
self.invalid += 1
elif line == b"":
self.cur_event = None
def run(self):
try:
s = socket.create_connection((args.host, 80), timeout=5)
s.sendall(REQ % args.host.encode())
self.end = read_chunked(s, time.monotonic() + self.seconds, self.on_line)
s.close()
except (OSError, ValueError) as e:
self.end = f"error: {e}"
clients = [Client(i, args.seconds) for i in range(args.clients)]
for c in clients:
c.start()
time.sleep(0.3)
stall_sock = None
if args.stall:
# The window scale is negotiated in the SYN, so the small buffer must be set before connect()
stall_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
stall_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2048)
stall_sock.settimeout(5)
stall_sock.connect((args.host, 80))
stall_sock.sendall(REQ % args.host.encode())
print("stalled client opened (never reads)", flush=True)
churn_done = 0
for _ in range(args.churn):
try:
cs = socket.create_connection((args.host, 80), timeout=5)
cs.sendall(REQ % args.host.encode())
cs.settimeout(1.0)
with contextlib.suppress(TimeoutError):
cs.recv(4096)
cs.setsockopt(
socket.SOL_SOCKET, socket.SO_LINGER, b"\x01\x00\x00\x00\x00\x00\x00\x00"
) # RST on close
cs.close()
churn_done += 1
except OSError as e:
print("churn error", e, flush=True)
time.sleep(2)
if args.churn:
print(f"churn: {churn_done}/{args.churn} abrupt closes", flush=True)
for c in clients:
c.join()
stall_closed = None
if stall_sock is not None:
# Drain what the small buffer holds: a close shows as b"" or a reset after it. A session the device left
# open keeps feeding events, so the drain has its own deadline and data past it means "still open".
stall_sock.settimeout(2.0)
drain_deadline = time.monotonic() + 5.0
try:
while stall_sock.recv(65536):
if time.monotonic() > drain_deadline:
break
else:
stall_closed = True
except TimeoutError:
pass
except OSError:
stall_closed = True
if stall_closed is None:
stall_closed = False
print(
"stalled client: closed by device"
if stall_closed
else "stalled client: still open",
flush=True,
)
stall_sock.close()
for c in clients:
states = c.events["state"]
print(
f"client {c.idx}: {c.end}, {states} state events, {c.invalid} invalid JSON, max data {c.max_data} B, "
f"{c.bytes} B, other events { {k: v for k, v in c.events.items() if k != 'state'} }",
flush=True,
)
print(" per id:", dict(sorted(c.ids.items())), flush=True)
failed = [
c for c in clients if c.end != "deadline" or c.invalid or not c.events["state"]
]
if failed:
sys.exit(
f"{len(failed)} client(s) did not run clean to the deadline with valid state events"
)
if stall_closed is False:
sys.exit("stalled session was not closed by the device")
if churn_done < args.churn:
sys.exit(f"only {churn_done} of {args.churn} churn connections were made")
@@ -0,0 +1,19 @@
#!/bin/sh
# sse_cases.sh HOST KEY LABEL CAPTURE : the SSE cases against templates/web-server-sse-dut.yaml with a console capture
# running into CAPTURE; KEY is the API key (- for plaintext). The burst client output goes to the working directory.
H=$1; K=$2; LABEL=$3; LOG=$4; PY=${PYTHON:-python3}; HERE=$(dirname "$0"); BTN=$HERE/../api_button.py
[ -r "$LOG" ] || { echo "usage: sse_cases.sh HOST KEY LABEL CAPTURE (CAPTURE must be a readable file)"; exit 2; }
show() { tail -n +$(($1+1)) "$LOG" | sed 's/\x1b\[[0-9;]*m//g' | grep -a '\[probe:\|\[W\]\|\[E\]\|web_server\|Guru\|abort\|Backtrace\|ESPHome version' | grep -a -v 'took a long time' | cut -c1-200; }
step() { n0=$(wc -l < "$LOG"); echo "=== $LABEL: $1 ($(date +%T))"; shift; "$@" || { rc=$?; echo "client failed ($rc), aborting"; exit $rc; }; echo "--- device log:"; show "$n0"; }
step "idle 15 s, no client" sleep 15
step "1 client 60 s" $PY "$HERE/sse.py" "$H" 60 --clients 1
step "4 clients 60 s" $PY "$HERE/sse.py" "$H" 60 --clients 4
step "1 reader plus a stalled client 45 s" $PY "$HERE/sse.py" "$H" 45 --clients 1 --stall
step "1 reader plus 10 abrupt closes 30 s" $PY "$HERE/sse.py" "$H" 30 --clients 1 --churn 10
n0=$(wc -l < "$LOG"); echo "=== $LABEL: 2 clients 25 s with the Burst button at 5 s ($(date +%T))"
$PY "$HERE/sse.py" "$H" 25 --clients 2 > "$PWD/burst-$LABEL.txt" 2>&1 &
pid=$!
sleep 5; $PY "$BTN" "$H" "$K" press "Burst" || { echo "button press failed"; exit 1; }
wait $pid || { cat "$PWD/burst-$LABEL.txt"; echo "SSE clients failed during the burst"; exit 1; }
cat "$PWD/burst-$LABEL.txt"; echo "--- device log:"; show "$n0"
step "idle 15 s after" sleep 15
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# ssecurl.sh HOST USER PASS SECONDS OUT : one authenticated SSE client through curl; counts state events and checks JSON
H=$1; U=$2; P=$3; T=$4; OUT=$5
timeout "$T" curl -sS -f -N -u "$U:$P" --anyauth "http://$H/events" > "$OUT"; rc=$?
[ "$rc" = 124 ] || { echo "sse: curl ended with status $rc before the $T s deadline"; exit 1; }
python3 - "$OUT" <<'PY'
import json, sys
states = invalid = 0; biggest = 0; ev = None
data = open(sys.argv[1], "rb").read()
lines = data.split(b"\n")
if not data.endswith(b"\n"):
lines = lines[:-1] # the deadline can cut the last line in half; only complete lines are checked
for line in lines:
line = line.rstrip(b"\r")
if line.startswith(b"event:"):
ev = line[6:].strip()
elif line.startswith(b"data:") and ev == b"state":
states += 1; d = line[5:].strip(); biggest = max(biggest, len(d))
try: json.loads(d)
except ValueError: invalid += 1
elif line == b"":
ev = None
print(f"sse: {states} state events, {invalid} invalid JSON, largest {biggest} B")
sys.exit(1 if invalid or not states else 0)
PY
@@ -0,0 +1,66 @@
# Needs packages/aux-2-pre-prod.yaml from NabuCasa/aux-2 at commit 283f2e64 next to this file (see
# reference/recipes/usb_audio.md). The AUX-2 package with the package's speaker_source player swapped for the speaker
# platform player, which is the one built on speaker/media_player/audio_pipeline.cpp, plus the task name probe.
packages:
core: !include aux-2-pre-prod.yaml
esphome:
name: aux-2-dut
name_add_mac_suffix: false
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
encryption:
key: !secret api_key
logger:
level: DEBUG
sendspin:
unpaired_access: true
media_player:
- id: !remove external_media_player
- platform: speaker
id: speaker_media_player
name: Media Player
volume_increment: 0.05
volume_min: 0.4
volume_max: 0.85
announcement_pipeline:
speaker: announcement_resampling_speaker
format: FLAC
num_channels: 1
sample_rate: 48000
media_pipeline:
speaker: media_resampling_speaker
format: FLAC
num_channels: 2
sample_rate: 48000
on_announcement:
- mixer_speaker.apply_ducking:
id: media_mixing_input
decibel_reduction: 20
duration: 0.0s
button:
- platform: template
name: Task names
on_press:
- lambda: |-
static const char *const NAMES[] = {"ann_read", "ann_decode", "med_read", "med_decode"};
for (const char *n : NAMES) {
TaskHandle_t h = xTaskGetHandle(n);
if (h == nullptr) {
ESP_LOGI("tasks", "%s: missing", n);
} else {
ESP_LOGI("tasks", "%s: found as '%s', stack high water %u", n, pcTaskGetName(h),
(unsigned) uxTaskGetStackHighWaterMark(h));
}
}
ESP_LOGI("tasks", "internal free %u, largest block %u", (unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
(unsigned) heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
@@ -0,0 +1,88 @@
# MQTT transport test (#16056, needs that PR's tree for the transport option): one build per transport, name and mqtt block per file (see reference/recipes/mqtt.md).
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: VERBOSE
logs:
wifi: DEBUG
api: DEBUG
api.connection: DEBUG
scheduler: DEBUG
component: DEBUG
sensor: DEBUG
esp32: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
ota:
- platform: esphome
interval:
- interval: 10s
then:
- lambda: |-
ESP_LOGI("probe", "mqtt connected %d, internal free %u, largest block %u", id(mq).is_connected(),
(unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
(unsigned) heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
sensor:
- platform: template
name: Counter
update_interval: 2s
accuracy_decimals: 0
lambda: |-
static uint32_t n = 0;
return n++;
switch:
- platform: template
name: Relay
id: relay
optimistic: true
on_turn_on:
- logger.log: "Relay ON"
on_turn_off:
- logger.log: "Relay OFF"
button:
- platform: template
name: Publish big
on_press:
- mqtt.publish:
topic: ${name}/big
payload: !lambda |-
return std::string(3000, 'q');
substitutions:
name: mqtt-dut
esphome:
name: ${name}
mqtt:
id: mq
broker: !secret broker_ip # test.mosquitto.org for the public bundle run
port: 9002 # 1883 tcp, 9001 ws, 9002 wss, 8081 public wss
transport: wss # drop for tcp
ws_path: /mqtt # ws and wss only
certificate_authority: !secret mqtt_ca # drop for the bundle runs
skip_cert_cn_check: true # with the local CA only: the certificate names an address
reboot_timeout: 0s
discovery: false
on_message:
- topic: ${name}/cmd
then:
- logger.log:
format: "cmd payload: %s"
args: ["x.c_str()"]
@@ -0,0 +1,136 @@
# web_server_idf state SSE and HTTPD task stress (#21109, #21038). Add `auth:` with `type: digest` for the HTTPD
# stack case; the entity endpoints are named after the entities (/sensor/Fast%201, /switch/Toggle/toggle).
esphome:
name: web-server-dut
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
ota:
- platform: esphome
web_server:
port: 80
version: 3
# Stack and heap probe from the main loop task (loopTask) and the HTTPD task.
interval:
- interval: 5s
then:
- lambda: |-
TaskHandle_t httpd = xTaskGetHandle("httpd");
if (httpd == nullptr) {
ESP_LOGW("probe", "httpd task not found, loopTask min free stack %u",
(unsigned) uxTaskGetStackHighWaterMark(nullptr));
return;
}
ESP_LOGI("probe", "loopTask min free stack %u, httpd min free stack %u, internal free %u, largest block %u",
(unsigned) uxTaskGetStackHighWaterMark(nullptr), (unsigned) uxTaskGetStackHighWaterMark(httpd),
(unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
(unsigned) heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
# Small state events: eight sensors at 200 ms, well under the 1024 byte stack buffer.
sensor:
- platform: template
name: Fast 1
id: fast1
update_interval: 200ms
accuracy_decimals: 3
lambda: return random_float() * 100.0f;
- platform: template
name: Fast 2
update_interval: 200ms
accuracy_decimals: 3
lambda: return random_float() * 100.0f;
- platform: template
name: Fast 3
update_interval: 200ms
accuracy_decimals: 3
lambda: return random_float() * 100.0f;
- platform: template
name: Fast 4
update_interval: 200ms
accuracy_decimals: 3
lambda: return random_float() * 100.0f;
- platform: template
name: Fast 5
update_interval: 200ms
accuracy_decimals: 3
lambda: return random_float() * 100.0f;
- platform: template
name: Fast 6
update_interval: 200ms
accuracy_decimals: 3
lambda: return random_float() * 100.0f;
- platform: template
name: Fast 7
update_interval: 200ms
accuracy_decimals: 3
lambda: return random_float() * 100.0f;
- platform: template
name: Fast 8
update_interval: 200ms
accuracy_decimals: 3
lambda: return random_float() * 100.0f;
# Large state events: the text sensor JSON carries the string twice (value and state).
# mid ~1.3 kB goes to the 2048 byte tail, big ~3.7 kB grows the tail to 4096, huge ~5.7 kB exceeds the
# 5120 byte JSON room and must be dropped with "State event does not fit".
text_sensor:
- platform: template
name: Mid
id: mid
update_interval: 1s
lambda: |-
static uint32_t n = 0;
std::string s = std::to_string(n++) + ":";
s.append(600 - s.size(), 'm');
return s;
- platform: template
name: Big
id: big
update_interval: 2s
lambda: |-
static uint32_t n = 0;
std::string s = std::to_string(n++) + ":";
s.append(1800 - s.size(), 'b');
return s;
- platform: template
name: Huge
id: huge
update_interval: 7s
lambda: |-
static uint32_t n = 0;
std::string s = std::to_string(n++) + ":";
s.append(2800 - s.size(), 'h');
return s;
switch:
- platform: template
name: Toggle
optimistic: true
button:
- platform: template
name: Burst
on_press:
- lambda: |-
for (int i = 0; i < 50; i++) {
id(fast1).publish_state(i);
id(mid).publish_state(std::string(600, 'x'));
}
-4
View File
@@ -28,7 +28,6 @@
// python
"ms-python.python",
"ms-python.pylint",
"ms-python.flake8",
"charliermarsh.ruff",
"visualstudioexptteam.vscodeintellicode",
// yaml
@@ -48,9 +47,6 @@
"pylint.args": [
"--rcfile=${workspaceFolder}/pyproject.toml"
],
"flake8.args": [
"--config=${workspaceFolder}/.flake8"
],
"ruff.configuration": "${workspaceFolder}/pyproject.toml",
"[python]": {
// VS will say "Value is not accepted" before building the devcontainer, but the warning
-42
View File
@@ -1,42 +0,0 @@
[flake8]
max-line-length = 120
# Following 4 for black compatibility
# E501: line too long
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
# TODO fix flake8
# D100 Missing docstring in public module
# D101 Missing docstring in public class
# D102 Missing docstring in public method
# D103 Missing docstring in public function
# D104 Missing docstring in public package
# D105 Missing docstring in magic method
# D107 Missing docstring in __init__
# D200 One-line docstring should fit on one line with quotes
# D205 1 blank line required between summary line and description
# D209 Multi-line docstring closing quotes should be on a separate line
# D400 First line should end with a period
# D401 First line should be in imperative mood
ignore =
E501,
W503,
E203,
D202,
D100,
D101,
D102,
D103,
D104,
D105,
D107,
D200,
D205,
D209,
D400,
D401,
exclude = api_pb2.py
+3 -3
View File
@@ -1007,19 +1007,19 @@ jobs:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 S3
# yamllint disable-line rule:line-length
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM --grep USE_OPENTHREAD
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep SOC_RISCV_COPROC_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM --grep USE_OPENTHREAD
tidy_environment: esp32s3-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
# so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep SOC_LP_CORE_SUPPORTED --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
tidy_environment: esp32p4-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 C6
# yamllint disable-line rule:line-length
options: --environment esp32c6-idf-tidy --grep SOC_LP_I2C_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
options: --environment esp32c6-idf-tidy --grep SOC_LP_I2C_SUPPORTED --grep SOC_LP_CORE_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
tidy_environment: esp32c6-idf-tidy
steps:
@@ -1,39 +0,0 @@
{
"problemMatcher": [
{
"owner": "ruff",
"severity": "error",
"pattern": [
{
"regexp": "^(.*): (Please format this file with the ruff formatter)",
"file": 1,
"message": 2
}
]
},
{
"owner": "flake8",
"severity": "error",
"pattern": [
{
"regexp": "^(.*):(\\d+): ([EFCDNW]\\d{3}.*)$",
"file": 1,
"line": 2,
"message": 3
}
]
},
{
"owner": "pylint",
"severity": "error",
"pattern": [
{
"regexp": "^(.*):(\\d+): (\\[[EFCRW]\\d{4}\\(.*\\),.*\\].*)$",
"file": 1,
"line": 2,
"message": 3
}
]
}
]
}
+1 -1
View File
@@ -85,7 +85,7 @@ jobs:
- name: Verify prek clean
# Second pass: re-run all hooks against the now-fixed tree.
# Auto-fixers exit 0 (nothing to change); any remaining failure
# from a check-only hook (flake8 / yamllint / ci-custom) is a
# from a check-only hook (yamllint / ci-custom) is a
# real issue and fails the workflow loudly. Same PREK_SKIP list as
# above for the same reasons.
env:
+1 -9
View File
@@ -17,14 +17,6 @@ repos:
args: [--fix]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/PyCQA/flake8
rev: 7.4.1
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.7.0
- pydocstyle==5.1.1
files: ^(esphome|tests)/.+\.py$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
@@ -46,7 +38,7 @@ repos:
- id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v13.0.1
rev: v22.1.8
hooks:
- id: clang-format
types_or: [c, c++]
+1 -1
View File
@@ -55,7 +55,7 @@ file does, and it is the authority when they disagree. The most useful starting
* https://developers.esphome.io/contributing/code/ - contribution rules, public API and breaking changes.
* **Formatting:**
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
* **Python:** Uses `ruff` for linting and formatting. Configuration is in `pyproject.toml`.
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
* **Naming Conventions:**
+2
View File
@@ -205,6 +205,7 @@ esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_power_management/* @rwrozelle
esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp32_ulp/* @swoboda1337
esphome/components/esp8266/* @esphome/core
esphome/components/esp_ldo/* @clydebarrow
esphome/components/espectre/* @francescopace
@@ -697,6 +698,7 @@ esphome/components/veml3235/* @kbx81
esphome/components/veml7700/* @latonita
esphome/components/version/* @esphome/core
esphome/components/vevor_dtx/* @Bofr5555
esphome/components/vl53l1x/* @will-tm
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.25.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.26.0
RUN \
platformio settings set enable_telemetry No \
+4
View File
@@ -8,12 +8,14 @@ import textwrap
from esphome.build_helpers import pch
from esphome.components.esp32 import (
KEY_SRC_CMAKE,
get_esp32_variant,
get_excluded_builtin_components,
get_managed_component_require_names,
idf_version,
)
import esphome.config_validation as cv
from esphome.const import KEY_ESP32
from esphome.core import CORE
from esphome.espidf import variant_to_idf_target
from esphome.framework_helpers import (
@@ -460,6 +462,7 @@ def get_component_cmakelists() -> str:
# get_project_cmakelists so they reach every component, not just src/.
link_opts = get_project_link_flags()
link_opts_str = "\n ".join(link_opts) if link_opts else ""
src_cmake = "\n".join(CORE.data[KEY_ESP32].get(KEY_SRC_CMAKE, []))
return f"""\
# Auto-generated by ESPHome
@@ -505,6 +508,7 @@ idf_component_register(
target_link_options(${{COMPONENT_LIB}} PUBLIC
{link_opts_str}
)
{src_cmake}
{_pch_cmake_block()}"""
+10 -18
View File
@@ -184,11 +184,9 @@ void AcDimmer::setup() {
this->store_.zero_cross_pin_number = this->zero_cross_pin_->get_pin();
this->store_.min_power = static_cast<uint16_t>(this->min_power_ * 1000);
this->min_power_ = 0;
this->store_.method = this->method_;
if (setup_zero_cross_pin) {
this->zero_cross_pin_->setup();
this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr();
this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_,
this->zero_cross_interrupt_type_);
}
@@ -227,24 +225,18 @@ void AcDimmer::dump_config() {
ESP_LOGCONFIG(TAG,
"AcDimmer:\n"
" Min Power: %.1f%%\n"
" Init with half cycle: %s",
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
" Init with half cycle: %s\n"
" Interrupt Type: %s\n"
" Method: %s",
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_),
this->zero_cross_interrupt_type_ == gpio::INTERRUPT_RISING_EDGE ? LOG_STR_LITERAL("rising")
: this->zero_cross_interrupt_type_ == gpio::INTERRUPT_FALLING_EDGE ? LOG_STR_LITERAL("falling")
: LOG_STR_LITERAL("any"),
this->store_.method == DIM_METHOD_LEADING_PULSE ? LOG_STR_LITERAL("leading pulse")
: this->store_.method == DIM_METHOD_LEADING ? LOG_STR_LITERAL("leading")
: LOG_STR_LITERAL("trailing"));
LOG_PIN(" Output Pin: ", this->gate_pin_);
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_RISING_EDGE) {
ESP_LOGCONFIG(TAG, " Interrupt Type: rising");
} else if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_FALLING_EDGE) {
ESP_LOGCONFIG(TAG, " Interrupt Type: falling");
} else {
ESP_LOGCONFIG(TAG, " Interrupt Type: any");
}
if (method_ == DIM_METHOD_LEADING_PULSE) {
ESP_LOGCONFIG(TAG, " Method: leading pulse");
} else if (method_ == DIM_METHOD_LEADING) {
ESP_LOGCONFIG(TAG, " Method: leading");
} else {
ESP_LOGCONFIG(TAG, " Method: trailing");
}
LOG_FLOAT_OUTPUT(this);
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
}
+9 -12
View File
@@ -6,19 +6,11 @@
namespace esphome::ac_dimmer {
enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
enum DimMethod : uint8_t { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
struct AcDimmerDataStore {
/// Zero-cross pin
ISRInternalGPIOPin zero_cross_pin;
/// Zero-cross pin number - used to share ZC pin across multiple dimmers
uint8_t zero_cross_pin_number;
/// Output pin to write to
ISRInternalGPIOPin gate_pin;
/// Value of the dimmer - 0 to 65535.
uint16_t value;
/// Minimum power for activation
uint16_t min_power;
/// Time between the last two ZC pulses
uint32_t cycle_time_us;
/// Time (in micros()) of last ZC signal
@@ -27,6 +19,12 @@ struct AcDimmerDataStore {
uint32_t enable_time_us;
/// Time since last ZC pulse to disable gate pin. 0 means no disable.
uint32_t disable_time_us;
/// Value of the dimmer - 0 to 65535.
uint16_t value;
/// Minimum power for activation
uint16_t min_power;
/// Zero-cross pin number - used to share ZC pin across multiple dimmers
uint8_t zero_cross_pin_number;
/// Set to send the first half ac cycle complete
bool init_cycle;
/// Dimmer method
@@ -50,17 +48,16 @@ class AcDimmer final : public output::FloatOutput, public Component {
void set_zero_cross_pin(InternalGPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; }
void set_zero_cross_interrupt_type(gpio::InterruptType type) { zero_cross_interrupt_type_ = type; }
void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; }
void set_method(DimMethod method) { method_ = method; }
void set_method(DimMethod method) { this->store_.method = method; }
protected:
void write_state(float state) override;
InternalGPIOPin *gate_pin_;
InternalGPIOPin *zero_cross_pin_;
gpio::InterruptType zero_cross_interrupt_type_;
AcDimmerDataStore store_;
gpio::InterruptType zero_cross_interrupt_type_;
bool init_with_half_cycle_;
DimMethod method_;
};
} // namespace esphome::ac_dimmer
+8 -8
View File
@@ -94,11 +94,11 @@ void ADCSensor::setup() {
}
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT,
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
.default_vref = 1100, // Default reference voltage in mV
.default_vref = 1100, // Default reference voltage in mV
#endif // !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
};
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
@@ -248,11 +248,11 @@ float ADCSensor::sample_autorange_() {
(err == ESP_OK) ? "SUCCESS" : "FAILED", err);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT,
.unit_id = this->adc_unit_,
.atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
.default_vref = 1100,
.default_vref = 1100,
#endif
};
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
+1 -2
View File
@@ -2,7 +2,7 @@ from esphome import automation, pins
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.components.const import CONF_AQI
from esphome.components.const import CONF_AQI, CONF_ERROR_CODE
import esphome.config_validation as cv
from esphome.const import (
CONF_ECO2,
@@ -55,7 +55,6 @@ CONF_RAW_HUMIDITY = "raw_humidity"
CONF_RS0 = "rs0"
CONF_RS2 = "rs2"
CONF_RS3 = "rs3"
CONF_ERROR_CODE = "error_code"
apc1_ns = cg.esphome_ns.namespace("apc1")
APC1Component = apc1_ns.class_("APC1Component", uart.UARTDevice, cg.Component)
+11 -37
View File
@@ -7,7 +7,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_DESCRIPTION, CONF_HOST
from esphome.components.logger import request_log_listener
from esphome.components.network import DOMAIN as NETWORK_DOMAIN
from esphome.components.network import require_ipv4, require_ipv6
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
# components and downstream consumers that import them from api
@@ -28,7 +28,6 @@ from esphome.const import (
CONF_DATA,
CONF_DATA_TEMPLATE,
CONF_DELAY,
CONF_ENABLE_IPV6,
CONF_ENCRYPTION,
CONF_EVENT,
CONF_ID,
@@ -52,7 +51,6 @@ from esphome.const import (
)
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import Expression, MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import fnv1_hash
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigFragmentType, ConfigType
@@ -306,11 +304,6 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
def _validate_outgoing_connection(config: ConfigType) -> ConfigType:
if (outgoing := config.get(CONF_OUTGOING_CONNECTION)) is None:
return config
if CONF_ENCRYPTION not in config:
raise cv.Invalid(
"outgoing_connection requires 'encryption' so the peer is verified by key",
path=[CONF_OUTGOING_CONNECTION],
)
# A device with no client reboots once reboot_timeout passes, so a delay that
# reaches it would reboot the device before it ever dials
reboot_timeout = config[CONF_REBOOT_TIMEOUT]
@@ -338,7 +331,10 @@ def _validate_outgoing_host(value: str) -> IPv4Address | IPv6Address:
"give the address without the '%' part"
)
if (mapped := address.ipv4_mapped) is not None:
return mapped
address = mapped
# The build must carry the host's address family for the dial to work
require = require_ipv6 if isinstance(address, IPv6Address) else require_ipv4
require("api.outgoing_connection")(value)
return address
@@ -445,6 +441,11 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
cv.key_requires(
CONF_OUTGOING_CONNECTION,
CONF_ENCRYPTION,
hint="The peer is verified by its key",
),
_validate_outgoing_connection,
_consume_api_sockets,
_register_provisioning_source,
@@ -479,31 +480,12 @@ def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
return strings
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
if (
(outgoing := config.get(CONF_OUTGOING_CONNECTION)) is None
or (host := outgoing.get(CONF_HOST)) is None
or host.version != 6
):
return config
network_conf = fv.full_config.get().get(NETWORK_DOMAIN) or {}
if not network_conf.get(CONF_ENABLE_IPV6):
raise cv.Invalid(
"outgoing_connection host is an IPv6 address but IPv6 is not "
"enabled; set 'network: enable_ipv6: true'",
path=[CONF_OUTGOING_CONNECTION, CONF_HOST],
)
return config
def _validate_wizard(config: ConfigType) -> ConfigType:
wizard.final_validate(config)
return config
FINAL_VALIDATE_SCHEMA = cv.All(
_validate_outgoing_host_ipv6,
_validate_wizard,
)
@@ -728,15 +710,6 @@ VARIABLES_FIELDS_SCHEMA = cv.All(VARIABLES_SCHEMA, cv.Length(max=_FIELD_MAP_MAX)
def _validate_response_config(config: ConfigType) -> ConfigType:
# Validate dependencies:
# - response_template requires capture_response: true
# - capture_response: true requires on_success
if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
raise cv.Invalid(
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
path=[CONF_RESPONSE_TEMPLATE],
)
if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
raise cv.Invalid(
f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
@@ -771,6 +744,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
),
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION),
cv.only_when(CONF_RESPONSE_TEMPLATE, CONF_CAPTURE_RESPONSE, True),
_validate_response_config,
)
+3 -3
View File
@@ -683,7 +683,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "ConstVector<const char *, true>"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
}
// Deprecated in API version 1.6 - only used in deprecated fields
@@ -1331,9 +1331,9 @@ message ListEntitiesClimateResponse {
bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"];
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "ConstVector<const char *, true>"];
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "ConstVector<const char *, true>"];
bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 20;
+1 -1
View File
@@ -18,7 +18,7 @@
namespace esphome::api {
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
// #define HELPER_LOG_PACKETS
// Maximum message size limits to prevent OOM on constrained devices
// Handshake messages are limited to a small size for security
@@ -38,12 +38,14 @@ static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) {
const auto *bytes = reinterpret_cast<const uint8_t *>(&addr6->sin6_addr);
uint32_t prefix[3];
memcpy(prefix, bytes, sizeof(prefix));
#if USE_NETWORK_IPV4
// A dual-stack listener reports an IPv4 peer as ::ffff:a.b.c.d
if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == htonl(0xFFFFUL)) {
target.family = AF_INET;
memcpy(target.addr, bytes + sizeof(prefix), sizeof(struct in_addr));
return true;
}
#endif
// A link-local target is only reachable through the interface it came in
// on. Device platforms number interfaces from one; a host build can hand
// out an index too large to store, and a truncated one dials the wrong
@@ -57,6 +59,7 @@ static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) {
return true;
}
#endif
#if USE_NETWORK_IPV4
if (family != AF_INET) {
return false;
}
@@ -64,6 +67,9 @@ static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) {
target.family = AF_INET;
memcpy(target.addr, &addr4->sin_addr, sizeof(addr4->sin_addr));
return true;
#else
return false;
#endif
}
#endif
@@ -85,6 +91,7 @@ socklen_t OutgoingConnectionManager::target_sockaddr_(struct sockaddr_storage *a
return sizeof(*addr6);
}
#endif
#if USE_NETWORK_IPV4
if (this->saved_.family != AF_INET) {
return 0;
}
@@ -94,6 +101,9 @@ socklen_t OutgoingConnectionManager::target_sockaddr_(struct sockaddr_storage *a
addr4->sin_port = htons(API_OUTGOING_CONNECTION_PORT);
memcpy(&addr4->sin_addr, this->saved_.addr, sizeof(addr4->sin_addr));
return sizeof(*addr4);
#else
return 0;
#endif
#endif
}
+13 -24
View File
@@ -924,8 +924,7 @@ uint32_t ListEntitiesSensorResponse::calc_size_msg(const void *self) {
return size;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SensorStateResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
uint8_t *SensorStateResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const SensorStateResponse *>(self);
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.key);
if (uint32_t raw = float_to_raw(msg.state); raw != 0) [[likely]] {
@@ -938,8 +937,7 @@ SensorStateResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROT
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SensorStateResponse::calc_size_msg(const void *self) {
uint32_t SensorStateResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const SensorStateResponse *>(self);
uint32_t size = 0;
size += 5;
@@ -1096,8 +1094,7 @@ void SubscribeLogsRequest::decode_field(void *self, uint32_t tag, const uint8_t
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SubscribeLogsResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
uint8_t *SubscribeLogsResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const SubscribeLogsResponse *>(self);
pos = ProtoEncode::encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(msg.level));
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
@@ -1106,8 +1103,7 @@ SubscribeLogsResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PR
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SubscribeLogsResponse::calc_size_msg(const void *self) {
uint32_t SubscribeLogsResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const SubscribeLogsResponse *>(self);
uint32_t size = 0;
size += 2;
@@ -2448,8 +2444,8 @@ void SubscribeBluetoothLEAdvertisementsRequest::decode_field(void *self, uint32_
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
BluetoothLERawAdvertisementsResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
uint8_t *BluetoothLERawAdvertisementsResponse::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const BluetoothLERawAdvertisementsResponse *>(self);
for (uint16_t i = 0; i < msg.advertisements_len; i++) {
auto &sub_msg = msg.advertisements[i];
@@ -2472,8 +2468,7 @@ BluetoothLERawAdvertisementsResponse::encode_msg(const void *self, uint8_t *__re
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
BluetoothLERawAdvertisementsResponse::calc_size_msg(const void *self) {
uint32_t BluetoothLERawAdvertisementsResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const BluetoothLERawAdvertisementsResponse *>(self);
uint32_t size = 0;
for (uint16_t i = 0; i < msg.advertisements_len; i++) {
@@ -3820,15 +3815,13 @@ void ZWaveProxyFrame::decode_field(void *self, uint32_t tag, const uint8_t *data
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
ZWaveProxyFrame::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
uint8_t *ZWaveProxyFrame::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const ZWaveProxyFrame *>(self);
pos = ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.data, msg.data_len);
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
ZWaveProxyFrame::calc_size_msg(const void *self) {
uint32_t ZWaveProxyFrame::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const ZWaveProxyFrame *>(self);
uint32_t size = 0;
size += ProtoSize::calc_length(1, msg.data_len);
@@ -3943,8 +3936,7 @@ void InfraredRFTransmitRawTimingsRequest::decode_field(void *self, uint32_t tag,
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
InfraredRFReceiveEvent::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
uint8_t *InfraredRFReceiveEvent::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const InfraredRFReceiveEvent *>(self);
#ifdef USE_DEVICES
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.device_id);
@@ -3956,8 +3948,7 @@ InfraredRFReceiveEvent::encode_msg(const void *self, uint8_t *__restrict__ pos P
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
InfraredRFReceiveEvent::calc_size_msg(const void *self) {
uint32_t InfraredRFReceiveEvent::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const InfraredRFReceiveEvent *>(self);
uint32_t size = 0;
#ifdef USE_DEVICES
@@ -4061,16 +4052,14 @@ void SerialProxyConfigureRequest::decode_field(void *self, uint32_t tag, const u
}
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SerialProxyDataReceived::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
uint8_t *SerialProxyDataReceived::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const SerialProxyDataReceived *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.instance);
pos = ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.data_ptr_, msg.data_len_);
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SerialProxyDataReceived::calc_size_msg(const void *self) {
uint32_t SerialProxyDataReceived::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const SerialProxyDataReceived *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.instance);
+3 -3
View File
@@ -984,7 +984,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
bool supports_speed{false};
bool supports_direction{false};
int32_t supported_speed_count{0};
const std::vector<const char *> *supported_preset_modes{};
const ConstVector<const char *, true> *supported_preset_modes{};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -1777,9 +1777,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
bool supports_action{false};
const climate::ClimateFanModeMask *supported_fan_modes{};
const climate::ClimateSwingModeMask *supported_swing_modes{};
const std::vector<const char *> *supported_custom_fan_modes{};
const ConstVector<const char *, true> *supported_custom_fan_modes{};
const climate::ClimatePresetMask *supported_presets{};
const std::vector<const char *> *supported_custom_presets{};
const ConstVector<const char *, true> *supported_custom_presets{};
float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false};
bool supports_target_humidity{false};
+70 -70
View File
@@ -19,242 +19,242 @@ class APIServerConnectionBase {
public:
#endif
void on_hello_request(const HelloRequest &value){};
void on_hello_request(const HelloRequest &value) {};
void on_disconnect_request(const DisconnectRequest &value){};
void on_disconnect_response(){};
void on_ping_request(){};
void on_ping_response(){};
void on_device_info_request(){};
void on_disconnect_request(const DisconnectRequest &value) {};
void on_disconnect_response() {};
void on_ping_request() {};
void on_ping_response() {};
void on_device_info_request() {};
void on_device_capabilities_request(){};
void on_device_capabilities_request() {};
#ifdef USE_API_WIZARD
void on_device_wizard_request(){};
void on_device_wizard_request() {};
#endif
#ifdef USE_API_WIZARD_INPUTS
void on_wizard_input_set_request(const WizardInputSetRequest &value){};
void on_wizard_input_set_request(const WizardInputSetRequest &value) {};
#endif
void on_list_entities_request(){};
void on_list_entities_request() {};
void on_subscribe_states_request(){};
void on_subscribe_states_request() {};
#ifdef USE_COVER
void on_cover_command_request(const CoverCommandRequest &value){};
void on_cover_command_request(const CoverCommandRequest &value) {};
#endif
#ifdef USE_FAN
void on_fan_command_request(const FanCommandRequest &value){};
void on_fan_command_request(const FanCommandRequest &value) {};
#endif
#ifdef USE_LIGHT
void on_light_command_request(const LightCommandRequest &value){};
void on_light_command_request(const LightCommandRequest &value) {};
#endif
#ifdef USE_SWITCH
void on_switch_command_request(const SwitchCommandRequest &value){};
void on_switch_command_request(const SwitchCommandRequest &value) {};
#endif
void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
void on_subscribe_logs_request(const SubscribeLogsRequest &value) {};
#ifdef USE_API_NOISE
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value) {};
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request(){};
void on_subscribe_homeassistant_services_request() {};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
void on_homeassistant_action_response(const HomeassistantActionResponse &value) {};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request(){};
void on_subscribe_home_assistant_states_request() {};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
void on_home_assistant_state_response(const HomeAssistantStateResponse &value) {};
#endif
void on_get_time_response(const GetTimeResponse &value){};
void on_get_time_response(const GetTimeResponse &value) {};
#ifdef USE_API_USER_DEFINED_ACTIONS
void on_execute_service_request(const ExecuteServiceRequest &value){};
void on_execute_service_request(const ExecuteServiceRequest &value) {};
#endif
#ifdef USE_CAMERA
void on_camera_image_request(const CameraImageRequest &value){};
void on_camera_image_request(const CameraImageRequest &value) {};
#endif
#ifdef USE_CLIMATE
void on_climate_command_request(const ClimateCommandRequest &value){};
void on_climate_command_request(const ClimateCommandRequest &value) {};
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
void on_water_heater_command_request(const WaterHeaterCommandRequest &value) {};
#endif
#ifdef USE_NUMBER
void on_number_command_request(const NumberCommandRequest &value){};
void on_number_command_request(const NumberCommandRequest &value) {};
#endif
#ifdef USE_SELECT
void on_select_command_request(const SelectCommandRequest &value){};
void on_select_command_request(const SelectCommandRequest &value) {};
#endif
#ifdef USE_SIREN
void on_siren_command_request(const SirenCommandRequest &value){};
void on_siren_command_request(const SirenCommandRequest &value) {};
#endif
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &value){};
void on_lock_command_request(const LockCommandRequest &value) {};
#endif
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &value){};
void on_button_command_request(const ButtonCommandRequest &value) {};
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
void on_media_player_command_request(const MediaPlayerCommandRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &value){};
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_device_request(const BluetoothDeviceRequest &value){};
void on_bluetooth_device_request(const BluetoothDeviceRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){};
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){};
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){};
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){};
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){};
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){};
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value) {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_subscribe_bluetooth_connections_free_request(){};
void on_subscribe_bluetooth_connections_free_request() {};
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_unsubscribe_bluetooth_le_advertisements_request(){};
void on_unsubscribe_bluetooth_le_advertisements_request() {};
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){};
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value) {};
#endif
#ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){};
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value) {};
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_response(const VoiceAssistantResponse &value){};
void on_voice_assistant_response(const VoiceAssistantResponse &value) {};
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){};
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value) {};
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_audio(const VoiceAssistantAudio &value){};
void on_voice_assistant_audio(const VoiceAssistantAudio &value) {};
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){};
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value) {};
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){};
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value) {};
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value) {};
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value) {};
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){};
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value) {};
#endif
#ifdef USE_TEXT
void on_text_command_request(const TextCommandRequest &value){};
void on_text_command_request(const TextCommandRequest &value) {};
#endif
#ifdef USE_DATETIME_DATE
void on_date_command_request(const DateCommandRequest &value){};
void on_date_command_request(const DateCommandRequest &value) {};
#endif
#ifdef USE_DATETIME_TIME
void on_time_command_request(const TimeCommandRequest &value){};
void on_time_command_request(const TimeCommandRequest &value) {};
#endif
#ifdef USE_VALVE
void on_valve_command_request(const ValveCommandRequest &value){};
void on_valve_command_request(const ValveCommandRequest &value) {};
#endif
#ifdef USE_DATETIME_DATETIME
void on_date_time_command_request(const DateTimeCommandRequest &value){};
void on_date_time_command_request(const DateTimeCommandRequest &value) {};
#endif
#ifdef USE_UPDATE
void on_update_command_request(const UpdateCommandRequest &value){};
void on_update_command_request(const UpdateCommandRequest &value) {};
#endif
#ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){};
void on_z_wave_proxy_frame(const ZWaveProxyFrame &value) {};
#endif
#ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
void on_z_wave_proxy_request(const ZWaveProxyRequest &value) {};
#endif
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value) {};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){};
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value) {};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){};
void on_serial_proxy_write_request(const SerialProxyWriteRequest &value) {};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){};
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value) {};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){};
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value) {};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_request(const SerialProxyRequest &value){};
void on_serial_proxy_request(const SerialProxyRequest &value) {};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value) {};
#endif
#ifdef USE_SERIAL_PROXY
void on_subscribe_serial_proxy_identity_request(){};
void on_subscribe_serial_proxy_identity_request() {};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value) {};
#endif
#ifdef USE_SENDSPIN
void on_sendspin_pairing_token_request(){};
void on_sendspin_pairing_token_request() {};
#endif
};
+4 -1
View File
@@ -593,7 +593,10 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
std::function<void(StringRef)> &&f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
.entity_id = entity_id,
.attribute = attribute,
.callback = std::move(f),
.once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
});
}
@@ -20,7 +20,8 @@ namespace esphome::api {
// Converts a lambda result to the string sent to Home Assistant
template<typename T>
requires(!std::is_pointer_v<std::remove_cvref_t<T>>) std::string field_to_string(T &&val) {
requires(!std::is_pointer_v<std::remove_cvref_t<T>>)
std::string field_to_string(T &&val) {
return to_string(std::forward<T>(val)); // NOLINT
}
inline std::string field_to_string(const char *val) { return val ? std::string(val) : std::string(); }
+1 -5
View File
@@ -119,11 +119,6 @@ def _validate_multiple(conf: ConfigType) -> ConfigType:
"is only for standalone inputs (id)",
path=[CONF_MULTIPLE],
)
if CONF_MAX_LENGTH in conf and not conf[CONF_MULTIPLE]:
raise cv.Invalid(
"max_length only applies to an input with multiple: true",
path=[CONF_MAX_LENGTH],
)
return conf
@@ -150,6 +145,7 @@ WIZARD_INPUT_SCHEMA = cv.All(
),
cv.has_exactly_one_key(CONF_ID, CONF_ENTITY),
_validate_multiple,
cv.only_when(CONF_MAX_LENGTH, CONF_MULTIPLE, True),
)
WIZARD_PAGE_SCHEMA = cv.All(
+2 -6
View File
@@ -26,21 +26,17 @@ class CAQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}};
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
// clang-format off
{0.0f, 15.1f},
{15.1f, 30.1f},
{30.1f, 55.1f},
{55.1f, 110.1f}
// clang-format on
{55.1f, 110.1f},
};
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
// clang-format off
{0.0f, 25.1f},
{25.1f, 50.1f},
{50.1f, 90.1f},
{90.1f, 180.1f}
// clang-format on
{90.1f, 180.1f},
};
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
+7 -11
View File
@@ -17,16 +17,6 @@ DEPENDENCIES = ["sensor"]
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
def _validate_extended_range(config: ConfigType) -> ConfigType:
if CONF_EXTENDED_RANGE in config and config[CONF_CALCULATION_TYPE] == "CAQI":
raise cv.Invalid(
f"'{CONF_EXTENDED_RANGE}' is not supported with 'calculation_type: CAQI'. "
"CAQI has no maximum value by specification, so it is always reported unbounded.",
[CONF_EXTENDED_RANGE],
)
return config
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
AQISensor,
@@ -45,7 +35,13 @@ CONFIG_SCHEMA = cv.All(
}
)
.extend(cv.COMPONENT_SCHEMA),
_validate_extended_range,
cv.only_when(
CONF_EXTENDED_RANGE,
CONF_CALCULATION_TYPE,
"CAQI",
negate=True,
hint="CAQI has no maximum by specification, so it is always reported unbounded",
),
)
+1 -1
View File
@@ -157,7 +157,7 @@ class ATM90E32Component final : public PollingComponent,
#ifdef USE_NUMBER
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
#else
return 5.0f; // Default current
return 5.0f; // Default current
#endif
}
bool using_saved_calibrations_ = false; // Track if stored calibrations are being used
+1 -1
View File
@@ -22,7 +22,7 @@ class AudioStreamInfo {
*/
public:
AudioStreamInfo()
: AudioStreamInfo(16, 1, 16000){}; // Default values represent ESPHome's audio components historical values
: AudioStreamInfo(16, 1, 16000) {}; // Default values represent ESPHome's audio components historical values
AudioStreamInfo(uint8_t bits_per_sample, uint8_t channels, uint32_t sample_rate);
uint8_t get_bits_per_sample() const { return this->bits_per_sample_; }
+2 -4
View File
@@ -92,10 +92,8 @@ enum BedjetCommand : uint8_t {
};
#define BEDJET_FAN_STEP_NAMES_ \
{ \
"5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \
"85%", "90%", "95%", "100%" \
}
{"5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", \
"55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"}
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
@@ -61,7 +61,7 @@ void BedJetClimate::dump_config() {
void BedJetClimate::setup() {
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
this->set_supported_custom_fan_modes_static(BEDJET_FAN_STEP_NAMES, BEDJET_FAN_SPEED_COUNT);
this->set_supported_custom_presets({
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
"M1",
+1 -1
View File
@@ -20,7 +20,7 @@ class BedJetFan final : public fan::Fan, public BedJetClient, public PollingComp
/* BedJetClient status update */
void on_status(const BedjetStatusPacket *data) override;
void on_bedjet_state(bool is_ready) override{};
void on_bedjet_state(bool is_ready) override {};
std::string describe() override;
fan::FanTraits get_traits() override { return fan::FanTraits(false, true, false, BEDJET_FAN_SPEED_COUNT); }
@@ -20,18 +20,35 @@ extern "C" {
static const uint32_t SPI_TX_DMA_CHANNEL = GDMA_CHANNEL_3;
// TODO: Check if SPI_PERI_CLK_DCO depends on the chip variant
static const uint32_t SPI_PERI_CLK_26M = 26000000;
#if defined(USE_LIBRETINY_VARIANT_BK7251)
static const uint32_t SPI_PERI_CLK_DCO = 180000000;
#else
static const uint32_t SPI_PERI_CLK_DCO = 120000000;
#endif
// The beken378 SDK has two SPI register layouts:
// N-like (BK7231N, BK7238, BK7252N): CTRL +0x00, CONFIG +0x04, STAT +0x08, DAT +0x0C
// T-like (BK7231T, BK7251): CTRL +0x00, STAT +0x04, DAT +0x08
// The N-like variants also have TX enable / transfer length / TX finish bits in CONFIG.
// The T-like variants have no CONFIG address.
#if defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7238)
#define BEKEN_SPI_IS_N_LIKE
#endif
static const uint32_t SPI_BASE = 0x00802700;
#ifdef BEKEN_SPI_IS_N_LIKE
static const uint32_t SPI_DAT = SPI_BASE + 3 * 4;
static const uint32_t SPI_CONFIG = SPI_BASE + 1 * 4;
static const uint32_t SPI_TX_EN = 1 << 0;
static const uint32_t CTRL_NSSMD_3 = 1 << 17;
static const uint32_t SPI_TX_FINISH_EN = 1 << 2;
static const uint32_t SPI_RX_FINISH_EN = 1 << 3;
#else
static const uint32_t SPI_DAT = SPI_BASE + 2 * 4;
#endif
static const uint32_t CTRL_NSSMD_3 = 1 << 17;
namespace esphome::beken_spi_led_strip {
@@ -55,6 +72,7 @@ static void set_spi_ctrl_register(uint32_t bit, bool val) {
REG_WRITE(SPI_CTRL, value);
}
#ifdef BEKEN_SPI_IS_N_LIKE
static void set_spi_config_register(uint32_t bit, bool val) {
uint32_t value = REG_READ(SPI_CONFIG);
if (val == 0) {
@@ -64,10 +82,13 @@ static void set_spi_config_register(uint32_t bit, bool val) {
}
REG_WRITE(SPI_CONFIG, value);
}
#endif
void spi_dma_tx_enable(bool enable) {
GDMA_CFG_ST en_cfg;
#ifdef BEKEN_SPI_IS_N_LIKE
set_spi_config_register(SPI_TX_EN, enable);
#endif
en_cfg.channel = SPI_TX_DMA_CHANNEL;
en_cfg.param = enable ? 1 : 0;
sddev_control(GDMA_DEV_NAME, CMD_GDMA_SET_DMA_ENABLE, &en_cfg);
@@ -100,7 +121,13 @@ static void spi_set_clock(uint32_t max_hz) {
param = PCLK_POSI_SPI;
sddev_control(ICU_DEV_NAME, CMD_CONF_PCLK_26M, &param);
}
#ifdef BEKEN_SPI_IS_N_LIKE
// N-like variants: spi_clk = source_clk / (2 * div)
div = ((source_clk >> 1) / spi_clk);
#else
// T-like variants: spi_clk = source_clk / (2 * (div + 1))
div = ((source_clk >> 1) / spi_clk) - 1;
#endif
if (div < 2) {
div = 2;
} else if (div >= 255) {
@@ -110,7 +137,12 @@ static void spi_set_clock(uint32_t max_hz) {
param &= ~(SPI_CKR_MASK << SPI_CKR_POSI);
param |= (div << SPI_CKR_POSI);
REG_WRITE(SPI_CTRL, param);
ESP_LOGD(TAG, "target frequency: %" PRIu32 ", actual frequency: %d", max_hz, source_clk / 2 / div);
#ifdef BEKEN_SPI_IS_N_LIKE
const int divisor = div;
#else
const int divisor = div + 1;
#endif
ESP_LOGD(TAG, "target frequency: %" PRIu32 ", actual frequency: %d", max_hz, source_clk / 2 / divisor);
}
void spi_dma_tx_finish_callback(unsigned int param) {
@@ -187,8 +219,10 @@ void BekenSPILEDStripLightOutput::setup() {
set_spi_ctrl_register(TXINT_EN, false);
set_spi_ctrl_register(RXINT_EN, false);
#ifdef BEKEN_SPI_IS_N_LIKE
set_spi_config_register(SPI_TX_FINISH_EN, true);
set_spi_config_register(SPI_RX_FINISH_EN, true);
#endif
set_spi_ctrl_register(RXOVR_EN, false);
set_spi_ctrl_register(TXOVR_EN, false);
@@ -197,9 +231,17 @@ void BekenSPILEDStripLightOutput::setup() {
value |= (1 << 17);
REG_WRITE(SPI_CTRL, value);
#ifdef BEKEN_SPI_IS_N_LIKE
value = GFUNC_MODE_SPI_DMA;
#else
// For T-like variants, GFUNC_MODE_SPI_DMA identifies an SPI-DMA peripheral
// with a different base address. We want the regular SPI controller here.
value = GFUNC_MODE_SPI;
#endif
sddev_control(GPIO_DEV_NAME, CMD_GPIO_ENABLE_SECOND, &value);
#ifdef BEKEN_SPI_IS_N_LIKE
set_spi_ctrl_register(SPI_S_CS_UP_INT_EN, false);
#endif
GDMA_CFG_ST en_cfg;
GDMACFG_TPYES_ST init_cfg;
@@ -232,10 +274,16 @@ void BekenSPILEDStripLightOutput::setup() {
spi_dma_tx_enable(false);
#ifdef BEKEN_SPI_IS_N_LIKE
// N-like variants: The SPI controller must be told how many bytes to clock out.
value = REG_READ(SPI_CONFIG);
value &= ~(0xFFF << 8);
value |= ((dma_buffer_size & 0xFFF) << 8);
REG_WRITE(SPI_CONFIG, value);
#else
// T-like variants: No transfer-length register. The SPI controller shifts out
// everything the DMA writes into SPI_DAT.
#endif
}
void BekenSPILEDStripLightOutput::set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency) {
@@ -265,13 +313,35 @@ void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
}
if (!spi_data->first_run && !xSemaphoreTake(spi_data->dma_tx_semaphore, 10 / portTICK_PERIOD_MS)) {
// Recover so the next update can try again instead of being stuck forever.
ESP_LOGE(TAG, "Timed out waiting for semaphore");
spi_dma_tx_enable(false);
spi_data->tx_in_progress = false;
xSemaphoreGive(spi_data->dma_tx_semaphore);
this->status_set_warning();
this->schedule_show();
return;
}
#ifndef BEKEN_SPI_IS_N_LIKE
if (!spi_data->first_run) {
// For T-like variants, the GDMA finish interrupt only means that all bytes were written into
// the SPI TX FIFO. We must still wait for it to drain before refilling it.
uint32_t wait_start = micros();
while (!(REG_READ(SPI_STAT) & TXFIFO_EMPTY)) {
if (micros() - wait_start > 2000) {
ESP_LOGW(TAG, "SPI TX FIFO did not drain in time");
break;
}
}
}
#endif
if (spi_data->tx_in_progress) {
ESP_LOGE(TAG, "tx_in_progress is set");
xSemaphoreGive(spi_data->dma_tx_semaphore);
this->status_set_warning();
this->schedule_show();
return;
}
@@ -32,19 +32,12 @@ class LEDStripTimings:
spi_frequency: int
# The SPI clock divider depends on the chip family and is calculated in led_strip.cpp.
CHIPSETS = {
"WS2812": LEDStripTimings(
0b11100000, 0b11111100, 6666666
), # Clock divider: 9, Bit time: 1350ns
"SK6812": LEDStripTimings(
0b11000000, 0b11111000, 7500000
), # Clock divider: 8, Bit time: 1200ns
"APA106": LEDStripTimings(
0b11000000, 0b11111110, 5454545
), # Clock divider: 11, Bit time: 1650ns
"SM16703": LEDStripTimings(
0b11000000, 0b11111110, 7500000
), # Clock divider: 8, Bit time: 1200ns
"WS2812": LEDStripTimings(0b11100000, 0b11111100, 6666666),
"SK6812": LEDStripTimings(0b11000000, 0b11111000, 7500000),
"APA106": LEDStripTimings(0b11000000, 0b11111110, 5454545),
"SM16703": LEDStripTimings(0b11000000, 0b11111110, 7500000),
}
@@ -9,11 +9,9 @@ ESPHOME_LOG_TAG(TAG, "binary_sensor");
// Function implementation of LOG_BINARY_SENSOR macro to reduce code size
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) {
if (obj == nullptr) {
if (!log_entity_header(tag, prefix, type, obj)) {
return;
}
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
}
+12 -20
View File
@@ -91,23 +91,6 @@ def calculate_energy_reference(power_reference: float) -> float:
return power_reference * 3600000 / (1638.4 * 256)
def validate_legacy_mode(config: ConfigType) -> ConfigType:
# Only allow schematic calibration options if legacy_mode is False
if config.get(CONF_LEGACY_MODE, True):
forbidden = [
CONF_REFERENCE_VOLTAGE,
CONF_RESISTOR_SHUNT,
CONF_RESISTOR_ONE,
CONF_RESISTOR_TWO,
]
for key in forbidden:
if key in config:
raise cv.Invalid(
f"Option '{key}' is only allowed when legacy_mode: false"
)
return config
def set_command_defaults(config: ConfigType) -> ConfigType:
# Set defaults for read_command and write_command based on legacy_mode
legacy = config.get(CONF_LEGACY_MODE, True)
@@ -153,7 +136,7 @@ def set_reference_values(config: ConfigType) -> ConfigType:
return config
CONFIG_SCHEMA = (
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BL0940),
@@ -208,9 +191,18 @@ CONFIG_SCHEMA = (
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA)
.add_extra(validate_legacy_mode)
.add_extra(set_command_defaults)
.add_extra(set_reference_values)
.add_extra(set_reference_values),
cv.only_when(
(
CONF_REFERENCE_VOLTAGE,
CONF_RESISTOR_SHUNT,
CONF_RESISTOR_ONE,
CONF_RESISTOR_TWO,
),
CONF_LEGACY_MODE,
False,
),
)
# BL0940 datasheet: 4800 baud, 8 data bits, no parity (stop bits are 1.5 -- not
+1 -1
View File
@@ -26,7 +26,7 @@ class BLEClient;
class BLEClientNode {
public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param){};
esp_ble_gattc_cb_param_t *param) {};
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {}
virtual void loop() {}
void set_address(uint64_t address) { address_ = address; }
+12 -12
View File
@@ -104,19 +104,19 @@ struct HubCapabilities {
// transition for every accepted or refused mode request - consumers skip
// their own mode report on push builds.
template<typename T>
concept BLEHubContract = requires(T hub, ESPBTDeviceListener *listener, RawAdvertisementCallback raw_callback,
uint8_t *mac) {
hub.register_listener(listener);
hub.set_raw_advertisement_callback(raw_callback);
{ T::get_capabilities() } -> std::same_as<HubCapabilities>;
hub.get_adapter_mac(mac);
{ hub.scan_running() } -> std::same_as<bool>;
{ hub.scan_active() } -> std::same_as<bool>;
{ hub.request_scan_mode(true) } -> std::same_as<bool>;
concept BLEHubContract =
requires(T hub, ESPBTDeviceListener *listener, RawAdvertisementCallback raw_callback, uint8_t *mac) {
hub.register_listener(listener);
hub.set_raw_advertisement_callback(raw_callback);
{ T::get_capabilities() } -> std::same_as<HubCapabilities>;
hub.get_adapter_mac(mac);
{ hub.scan_running() } -> std::same_as<bool>;
{ hub.scan_active() } -> std::same_as<bool>;
{ hub.request_scan_mode(true) } -> std::same_as<bool>;
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
hub.set_scanner_state_callback(ScannerStateCallback{});
{ hub.get_scanner_state() } -> std::same_as<ScannerState>;
hub.set_scanner_state_callback(ScannerStateCallback{});
{ hub.get_scanner_state() } -> std::same_as<ScannerState>;
#endif
};
};
} // namespace esphome::ble_device_base
+9 -18
View File
@@ -94,24 +94,14 @@ def _esp32_config_schema() -> cv.All:
CONNECTION_SCHEMA = bluetooth_connection.hub_connection_schema(PLATFORM_ESP32)
def validate_connections(config: ConfigType) -> ConfigType:
if CONF_CONNECTIONS in config:
if not config[CONF_ACTIVE]:
raise cv.Invalid(
"Connections can only be used if the proxy is set to active"
)
elif config[CONF_ACTIVE]:
connection_slots: int = config[CONF_CONNECTION_SLOTS]
esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(
config
)
return {
**config,
CONF_CONNECTIONS: [
CONNECTION_SCHEMA({}) for _ in range(connection_slots)
],
}
return config
if CONF_CONNECTIONS in config or not config[CONF_ACTIVE]:
return config
connection_slots: int = config[CONF_CONNECTION_SLOTS]
esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
return {
**config,
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
}
return cv.All(
(
@@ -136,6 +126,7 @@ def _esp32_config_schema() -> cv.All:
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
),
cv.only_when(CONF_CONNECTIONS, CONF_ACTIVE, True),
validate_connections,
)
+12 -14
View File
@@ -139,12 +139,12 @@ void BME680BSECComponent::update_subscription_() {
uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR;
this->bsec_status_ =
bsec_update_subscription(virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings);
ESP_LOGV(TAG, "%s: updating subscription for %d virtual sensors (out=%d sensors)", this->device_id_.c_str(),
ESP_LOGV(TAG, "%s: updating subscription for %d virtual sensors (out=%d sensors)", this->device_id_,
num_virtual_sensors, num_sensor_settings);
}
void BME680BSECComponent::dump_config() {
ESP_LOGCONFIG(TAG, "%s via BSEC:", this->device_id_.c_str());
ESP_LOGCONFIG(TAG, "%s via BSEC:", this->device_id_);
bsec_version_t version;
bsec_get_version(&version);
@@ -208,7 +208,7 @@ void BME680BSECComponent::run_() {
return;
}
ESP_LOGV(TAG, "%s: Performing sensor run", this->device_id_.c_str());
ESP_LOGV(TAG, "%s: Performing sensor run", this->device_id_);
// Restore BSEC library state
// The reinit_bsec_lib_ method is computationally expensive: it takes 1200÷2900 microseconds on a ESP32.
@@ -265,7 +265,7 @@ void BME680BSECComponent::run_() {
}
void BME680BSECComponent::read_() {
ESP_LOGV(TAG, "%s: Reading data", this->device_id_.c_str());
ESP_LOGV(TAG, "%s: Reading data", this->device_id_);
int64_t curr_time_ns = this->get_time_ns_();
if (this->bme680_settings_.trigger_measurement) {
@@ -365,8 +365,7 @@ void BME680BSECComponent::read_() {
ESP_LOGW(TAG, "BSEC failed to process signals (BSEC Error Code %d)", this->bsec_status_);
return;
}
ESP_LOGV(TAG, "%s: after bsec_do_steps: num_inputs=%d num_outputs=%d", this->device_id_.c_str(), num_inputs,
num_outputs);
ESP_LOGV(TAG, "%s: after bsec_do_steps: num_inputs=%d num_outputs=%d", this->device_id_, num_inputs, num_outputs);
// Since we are about to go "out of scope" in the loop, take a snapshot of the state now so we can restore it later
if (BME680BSECComponent::instances.size() > 1)
@@ -381,7 +380,7 @@ void BME680BSECComponent::read_() {
}
void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) {
ESP_LOGV(TAG, "%s: Queuing sensor state publish actions", this->device_id_.c_str());
ESP_LOGV(TAG, "%s: Queuing sensor state publish actions", this->device_id_);
for (uint8_t i = 0; i < num_outputs; i++) {
float signal = outputs[i].signal;
switch (outputs[i].sensor_id) {
@@ -471,7 +470,7 @@ void BME680BSECComponent::snapshot_state_() {
this->bsec_status_ = bsec_get_state(0, this->bsec_state_data_, BSEC_MAX_STATE_BLOB_SIZE, this->work_buffer_,
sizeof(this->work_buffer_), &num_serialized_state);
if (this->bsec_status_ != BSEC_OK) {
ESP_LOGW(TAG, "%s: Failed to fetch BSEC library state for snapshot (BSEC Error Code %d)", this->device_id_.c_str(),
ESP_LOGW(TAG, "%s: Failed to fetch BSEC library state for snapshot (BSEC Error Code %d)", this->device_id_,
this->bsec_status_);
return;
}
@@ -482,7 +481,7 @@ void BME680BSECComponent::snapshot_state_() {
// Used to share the library when using more than one sensor
void BME680BSECComponent::restore_state_() {
if (!this->bsec_state_data_valid_) {
ESP_LOGV(TAG, "%s: BSEC state data NOT valid, aborting restore_state_()", this->device_id_.c_str());
ESP_LOGV(TAG, "%s: BSEC state data NOT valid, aborting restore_state_()", this->device_id_);
return;
}
@@ -527,17 +526,16 @@ void BME680BSECComponent::load_state_() {
return;
}
ESP_LOGV(TAG, "%s: Loading BSEC library state", this->device_id_.c_str());
ESP_LOGV(TAG, "%s: Loading BSEC library state", this->device_id_);
this->bsec_status_ =
bsec_set_state(this->bsec_state_data_, BSEC_MAX_STATE_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
if (this->bsec_status_ != BSEC_OK) {
ESP_LOGW(TAG, "%s: Failed to load BSEC library state (BSEC Error Code %d)", this->device_id_.c_str(),
this->bsec_status_);
ESP_LOGW(TAG, "%s: Failed to load BSEC library state (BSEC Error Code %d)", this->device_id_, this->bsec_status_);
return;
}
// All OK: set the BSEC state data as valid
this->bsec_state_data_valid_ = true;
ESP_LOGI(TAG, "%s: Loaded BSEC library state", this->device_id_.c_str());
ESP_LOGI(TAG, "%s: Loaded BSEC library state", this->device_id_);
}
void BME680BSECComponent::save_state_(uint8_t accuracy) {
@@ -552,7 +550,7 @@ void BME680BSECComponent::save_state_(uint8_t accuracy) {
if (!this->bsec_state_data_valid_)
return;
ESP_LOGV(TAG, "%s: Saving state", this->device_id_.c_str());
ESP_LOGV(TAG, "%s: Saving state", this->device_id_);
if (!this->bsec_state_.save(&this->bsec_state_data_)) {
ESP_LOGW(TAG, "Failed to save state");
+2 -2
View File
@@ -36,7 +36,7 @@ enum SampleRate {
class BME680BSECComponent final : public Component, public i2c::I2CDevice {
public:
void set_device_id(const std::string &devid) { this->device_id_.assign(devid); }
void set_device_id(const char *devid) { this->device_id_ = devid; }
void set_temperature_offset(float offset) { this->temperature_offset_ = offset; }
void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; }
void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; }
@@ -111,7 +111,7 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
uint32_t last_state_save_ms_ = 0;
bsec_bme_settings_t bme680_settings_;
std::string device_id_;
const char *device_id_{nullptr};
float temperature_offset_{0};
IAQMode iaq_mode_{IAQ_MODE_STATIC};
SupplyVoltage supply_voltage_;
+3 -3
View File
@@ -284,9 +284,9 @@ bool BMI270Component::update_data(motion::MotionData &data) {
if (this->is_failed())
return false;
// Accelerometer: registers 0x0C–0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb)
// When the magnetometer is active, the 8 aux data bytes (0x04–0x0B) sit directly before
// the accel data, so one burst read from AUX_X_LSB fetches both.
// Accelerometer: registers 0x0C–0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb)
// When the magnetometer is active, the 8 aux data bytes (0x04–0x0B) sit directly before
// the accel data, so one burst read from AUX_X_LSB fetches both.
#ifdef USE_BMI270_BMM150
const bool read_mag = this->magnetometer_ready_ && !this->magnetometer_callback_.empty();
uint8_t buf[BMI270_AUX_DATA_LEN + REG_READ_LEN];
+1 -3
View File
@@ -7,11 +7,9 @@ ESPHOME_LOG_TAG(TAG, "button");
// Function implementation of LOG_BUTTON macro to reduce code size
void log_button(const char *tag, const char *prefix, const char *type, Button *obj) {
if (obj == nullptr) {
if (!log_entity_header(tag, prefix, type, obj)) {
return;
}
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
LOG_ENTITY_ICON(tag, prefix, *obj);
}
+1 -1
View File
@@ -44,7 +44,7 @@ class ByteBuffer {
* Create a new Bytebuffer with the given capacity
*/
ByteBuffer(size_t capacity, Endian endianness = LITTLE)
: data_(std::vector<uint8_t>(capacity)), endianness_(endianness), limit_(capacity){};
: data_(std::vector<uint8_t>(capacity)), endianness_(endianness), limit_(capacity) {};
// templated functions to implement putting and getting data of various types. There are two flavours of all
// functions - one that uses the position as the offset, and updates the position accordingly, and one that
+2 -2
View File
@@ -85,7 +85,7 @@ struct CanStatus {
class Canbus : public Component {
public:
Canbus(){};
Canbus() {};
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
@@ -190,7 +190,7 @@ class CanbusTrigger final : public Trigger<std::vector<uint8_t>, uint32_t, bool>
public:
explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
const bool use_extended_id)
: parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){};
: parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id) {};
void set_remote_transmission_request(bool remote_transmission_request) {
this->remote_transmission_request_ = remote_transmission_request;
+18
View File
@@ -443,6 +443,24 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
await web_server.add_entity_config(var, web_server_config)
def set_custom_fan_modes(var: MockObj, modes: list[str]) -> None:
"""Point the climate at a shared flash table of custom fan modes."""
if modes:
table = cg.shared_progmem_array(
"climate_custom_fan_modes", cg.const_char_ptr, modes
)
cg.add(var.set_supported_custom_fan_modes_static(table, len(modes)))
def set_custom_presets(var: MockObj, presets: list[str]) -> None:
"""Point the climate at a shared flash table of custom presets."""
if presets:
table = cg.shared_progmem_array(
"climate_custom_presets", cg.const_char_ptr, presets
)
cg.add(var.set_supported_custom_presets_static(table, len(presets)))
async def register_climate(var: MockObj, config: ConfigType) -> None:
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
+5 -15
View File
@@ -479,10 +479,8 @@ void Climate::publish_state() {
ClimateTraits Climate::get_traits() {
auto traits = this->traits();
// Wire custom mode pointers from Climate-owned storage
if (this->supported_custom_fan_modes_)
traits.set_supported_custom_fan_modes_(this->supported_custom_fan_modes_);
if (this->supported_custom_presets_)
traits.set_supported_custom_presets_(this->supported_custom_presets_);
traits.set_supported_custom_fan_modes_(&this->supported_custom_fan_modes_);
traits.set_supported_custom_presets_(&this->supported_custom_presets_);
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
if (!std::isnan(this->visual_min_temperature_override_)) {
traits.set_visual_min_temperature(this->visual_min_temperature_override_);
@@ -628,7 +626,7 @@ template<typename T> bool set_primary_mode(optional<T> &primary, const char *&cu
* After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits)
*
* Lifetime Safety:
* - found_ptr must come from traits.find_custom_*_mode_()
* - found_ptr must come from Climate::find_custom_*_()
* - Only pointers found in traits are stored, ensuring they remain valid
* - Prevents dangling pointers from temporary strings
*
@@ -684,11 +682,7 @@ const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
}
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
if (this->supported_custom_fan_modes_) {
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
}
// Fallback for deprecated path: external components may set modes on ClimateTraits directly
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
@@ -696,11 +690,7 @@ const char *Climate::find_custom_preset_(const char *custom_preset) {
}
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
if (this->supported_custom_presets_) {
return vector_find(*this->supported_custom_presets_, custom_preset, len);
}
// Fallback for deprecated path: external components may set modes on ClimateTraits directly
return this->get_traits().find_custom_preset_(custom_preset, len);
return vector_find(this->supported_custom_presets_, custom_preset, len);
}
void Climate::dump_traits_(const char *tag) {
+19 -28
View File
@@ -12,10 +12,7 @@
namespace esphome::climate {
#define LOG_CLIMATE(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
}
#define LOG_CLIMATE(prefix, type, obj) log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))
class Climate;
@@ -246,26 +243,34 @@ class Climate : public EntityBase {
}
#endif
/// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits).
/// Set the supported custom fan modes; the list is copied (stored on Climate, referenced by ClimateTraits).
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
this->ensure_custom_fan_modes_().assign(modes.begin(), modes.end());
this->supported_custom_fan_modes_.assign_copy(modes.begin(), modes.size());
}
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->ensure_custom_fan_modes_() = modes;
this->supported_custom_fan_modes_.assign_copy(modes.data(), modes.size());
}
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->ensure_custom_fan_modes_().assign(modes, modes + N);
this->supported_custom_fan_modes_.assign_copy(modes, N);
}
/// Points at a static table that outlives the climate (codegen); call before any runtime copy.
void set_supported_custom_fan_modes_static(const char *const *modes, size_t count) {
this->supported_custom_fan_modes_.assign_static(modes, count);
}
/// Set the supported custom presets (stored on Climate, referenced by ClimateTraits).
/// Set the supported custom presets; the list is copied (stored on Climate, referenced by ClimateTraits).
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->ensure_custom_presets_().assign(presets.begin(), presets.end());
this->supported_custom_presets_.assign_copy(presets.begin(), presets.size());
}
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->ensure_custom_presets_() = presets;
this->supported_custom_presets_.assign_copy(presets.data(), presets.size());
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->ensure_custom_presets_().assign(presets, presets + N);
this->supported_custom_presets_.assign_copy(presets, N);
}
/// Points at a static table that outlives the climate (codegen); call before any runtime copy.
void set_supported_custom_presets_static(const char *const *presets, size_t count) {
this->supported_custom_presets_.assign_static(presets, count);
}
/// Check if a custom fan mode is currently active.
@@ -388,22 +393,8 @@ class Climate : public EntityBase {
#endif
private:
/// Lazy-allocate custom mode vectors (never freed — entity lives forever).
std::vector<const char *> &ensure_custom_fan_modes_() {
if (!this->supported_custom_fan_modes_) {
this->supported_custom_fan_modes_ = new std::vector<const char *>(); // NOLINT
}
return *this->supported_custom_fan_modes_;
}
std::vector<const char *> &ensure_custom_presets_() {
if (!this->supported_custom_presets_) {
this->supported_custom_presets_ = new std::vector<const char *>(); // NOLINT
}
return *this->supported_custom_presets_;
}
std::vector<const char *> *supported_custom_fan_modes_{nullptr};
std::vector<const char *> *supported_custom_presets_{nullptr};
ClimateCustomModes supported_custom_fan_modes_;
ClimateCustomModes supported_custom_presets_;
/** The active custom fan mode (private - enforces use of safe setters).
*
+7 -22
View File
@@ -2,31 +2,16 @@
namespace esphome::climate {
// Compat: shared empty vector for getters when no custom modes are set.
// Remove in 2026.11.0 when deprecated ClimateTraits setters are removed
// and getters can return const vector * instead of const vector &.
static const std::vector<const char *> EMPTY_CUSTOM_MODES; // NOLINT
ClimateTraits::ClimateTraits() = default;
const std::vector<const char *> &ClimateTraits::get_supported_custom_fan_modes() const {
if (this->supported_custom_fan_modes_) {
return *this->supported_custom_fan_modes_;
}
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
if (!this->compat_custom_fan_modes_.empty()) {
return this->compat_custom_fan_modes_;
}
return EMPTY_CUSTOM_MODES;
static const ClimateCustomModes EMPTY_CUSTOM_MODES; // NOLINT
const ClimateCustomModes &ClimateTraits::get_supported_custom_fan_modes() const {
return this->supported_custom_fan_modes_ != nullptr ? *this->supported_custom_fan_modes_ : EMPTY_CUSTOM_MODES;
}
const std::vector<const char *> &ClimateTraits::get_supported_custom_presets() const {
if (this->supported_custom_presets_) {
return *this->supported_custom_presets_;
}
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
if (!this->compat_custom_presets_.empty()) {
return this->compat_custom_presets_;
}
return EMPTY_CUSTOM_MODES;
const ClimateCustomModes &ClimateTraits::get_supported_custom_presets() const {
return this->supported_custom_presets_ != nullptr ? *this->supported_custom_presets_ : EMPTY_CUSTOM_MODES;
}
int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const {
+27 -107
View File
@@ -1,7 +1,7 @@
#pragma once
#include <cstring>
#include <vector>
#include <type_traits>
#include "climate_mode.h"
#include "esphome/core/finite_set_mask.h"
#include "esphome/core/helpers.h"
@@ -18,9 +18,12 @@ using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
/// Custom fan modes and custom presets: a flash table from codegen, or an owned copy set at runtime.
using ClimateCustomModes = ConstVector<const char *, true>;
// Lightweight linear search for small lists (1-20 items) of const char* pointers
// Avoids std::find template overhead
inline bool vector_contains(const std::vector<const char *> &vec, const char *value, size_t len) {
template<typename List> inline bool vector_contains(const List &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return true;
@@ -28,12 +31,12 @@ inline bool vector_contains(const std::vector<const char *> &vec, const char *va
return false;
}
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
template<typename List> inline bool vector_contains(const List &vec, const char *value) {
return vector_contains(vec, value, strlen(value));
}
// Find and return matching pointer from vector, or nullptr if not found
inline const char *vector_find(const std::vector<const char *> &vec, const char *value, size_t len) {
// Find and return matching pointer from a list, or nullptr if not found
template<typename List> inline const char *vector_find(const List &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return item;
@@ -71,9 +74,13 @@ inline const char *vector_find(const std::vector<const char *> &vec, const char
class Climate; // Forward declaration
class ClimateTraits {
friend class Climate; // Allow Climate to access protected find methods
friend class Climate; // Climate wires its custom mode lists into traits
public:
// Out of line on purpose: an inlined default init makes GCC copy the defaults from a .rodata
// template per traits() override, and .rodata is RAM on ESP8266.
ClimateTraits();
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; }
void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; }
@@ -93,42 +100,14 @@ class ClimateTraits {
if (!this->supported_fan_modes_.empty()) {
return true;
}
// Same precedence as get_supported_custom_fan_modes() getter
if (this->supported_custom_fan_modes_) {
return !this->supported_custom_fan_modes_->empty();
}
return !this->compat_custom_fan_modes_.empty(); // Compat: remove in 2026.11.0
return this->supported_custom_fan_modes_ != nullptr && !this->supported_custom_fan_modes_->empty();
}
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
this->compat_custom_fan_modes_ = modes;
}
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->compat_custom_fan_modes_ = modes;
}
// Remove before 2026.11.0
template<size_t N>
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->compat_custom_fan_modes_.assign(modes, modes + N);
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
const std::vector<const char *> &get_supported_custom_fan_modes() const;
/// Empty when the climate has no custom fan modes. Set them on the Climate entity.
const ClimateCustomModes &get_supported_custom_fan_modes() const;
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
return (this->supported_custom_fan_modes_ &&
vector_contains(*this->supported_custom_fan_modes_, custom_fan_mode)) ||
vector_contains(this->compat_custom_fan_modes_, custom_fan_mode); // Compat: remove in 2026.11.0
return vector_contains(this->get_supported_custom_fan_modes(), custom_fan_mode);
}
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supports_custom_fan_mode(custom_fan_mode.c_str());
@@ -140,32 +119,10 @@ class ClimateTraits {
bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->compat_custom_presets_ = presets;
}
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->compat_custom_presets_ = presets;
}
// Remove before 2026.11.0
template<size_t N>
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(const char *const (&presets)[N]) {
this->compat_custom_presets_.assign(presets, presets + N);
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
const std::vector<const char *> &get_supported_custom_presets() const;
/// Empty when the climate has no custom presets. Set them on the Climate entity.
const ClimateCustomModes &get_supported_custom_presets() const;
bool supports_custom_preset(const char *custom_preset) const {
return (this->supported_custom_presets_ && vector_contains(*this->supported_custom_presets_, custom_preset)) ||
vector_contains(this->compat_custom_presets_, custom_preset); // Compat: remove in 2026.11.0
return vector_contains(this->get_supported_custom_presets(), custom_preset);
}
bool supports_custom_preset(const std::string &custom_preset) const {
return this->supports_custom_preset(custom_preset.c_str());
@@ -232,38 +189,8 @@ class ClimateTraits {
}
/// Set custom mode pointers (only Climate::get_traits() should call these).
void set_supported_custom_fan_modes_(const std::vector<const char *> *modes) {
this->supported_custom_fan_modes_ = modes;
}
void set_supported_custom_presets_(const std::vector<const char *> *presets) {
this->supported_custom_presets_ = presets;
}
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
if (this->supported_custom_fan_modes_) {
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
}
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
return vector_find(this->compat_custom_fan_modes_, custom_fan_mode, len);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
const char *find_custom_preset_(const char *custom_preset) const {
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
if (this->supported_custom_presets_) {
return vector_find(*this->supported_custom_presets_, custom_preset, len);
}
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
return vector_find(this->compat_custom_presets_, custom_preset, len);
}
void set_supported_custom_fan_modes_(const ClimateCustomModes *modes) { this->supported_custom_fan_modes_ = modes; }
void set_supported_custom_presets_(const ClimateCustomModes *presets) { this->supported_custom_presets_ = presets; }
uint32_t feature_flags_{0};
float visual_min_temperature_{10};
@@ -279,17 +206,10 @@ class ClimateTraits {
climate::ClimatePresetMask supported_presets_;
TemperatureUnit temperature_unit_{TemperatureUnit::CELSIUS};
/** Custom mode storage - pointers to vectors owned by the Climate base class.
*
* ClimateTraits does not own this data; Climate stores the vectors and
* get_traits() wires these pointers automatically.
*/
const std::vector<const char *> *supported_custom_fan_modes_{nullptr};
const std::vector<const char *> *supported_custom_presets_{nullptr};
// Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector).
// Remove in 2026.11.0.
std::vector<const char *> compat_custom_fan_modes_;
std::vector<const char *> compat_custom_presets_;
/// Owned by the Climate entity; get_traits() wires these pointers.
const ClimateCustomModes *supported_custom_fan_modes_{nullptr};
const ClimateCustomModes *supported_custom_presets_{nullptr};
};
static_assert(std::is_trivially_copyable_v<ClimateTraits>, "ClimateTraits is copied on every traits() call");
} // namespace esphome::climate
+4
View File
@@ -11,9 +11,11 @@ CONF_ACCELEROMETER_RANGE = "accelerometer_range"
CONF_ADC_AVERAGING = "adc_averaging"
CONF_ADC_TIME = "adc_time"
CONF_ALLOWED_IPS = "allowed_ips"
CONF_ANNOUNCEMENT_VOLUME = "announcement_volume"
CONF_AQI = "aqi"
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
CONF_B_CONSTANT = "b_constant"
CONF_BITRATE = "bitrate"
CONF_BLUE_CHANNEL = "blue_channel"
CONF_BORDER_ROUTER = "border_router"
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
@@ -33,6 +35,7 @@ CONF_DRAW_ROUNDING = "draw_rounding"
CONF_DRIVE_STRENGTH = "drive_strength"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
CONF_ERROR_CODE = "error_code"
CONF_GREEN_CHANNEL = "green_channel"
CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
@@ -48,6 +51,7 @@ CONF_LIBRETINY = "libretiny"
CONF_LOOP = "loop"
CONF_MANUFACTURER = "manufacturer"
CONF_MAX_PACKET_SIZE = "max_packet_size"
CONF_MODULATION = "modulation"
CONF_NOX_INDEX = "nox_index"
CONF_ON_PACKET = "on_packet"
CONF_ON_RECEIVE = "on_receive"
+1 -2
View File
@@ -14,8 +14,7 @@ static constexpr float COVER_OPEN = 1.0f;
static constexpr float COVER_CLOSED = 0.0f;
#define LOG_COVER(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))) { \
auto traits_ = (obj)->get_traits(); \
if (traits_.get_is_assumed_state()) { \
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
+1 -2
View File
@@ -13,8 +13,7 @@
namespace esphome::datetime {
#define LOG_DATETIME_DATE(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))) { \
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
}
@@ -13,8 +13,7 @@
namespace esphome::datetime {
#define LOG_DATETIME_DATETIME(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))) { \
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
}
+1 -2
View File
@@ -13,8 +13,7 @@
namespace esphome::datetime {
#define LOG_DATETIME_TIME(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))) { \
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
}
+1 -4
View File
@@ -164,10 +164,6 @@ _WAKEUP_PIN_MODE_BOTH_LEVELS = (
def validate_config(config: ConfigType) -> ConfigType:
wakeup_pins = config.get(CONF_WAKEUP_PIN)
has_pin_mode = CONF_WAKEUP_PIN_MODE in config
if has_pin_mode and wakeup_pins is None:
raise cv.Invalid(
"'wakeup_pin_mode' requires 'wakeup_pin'", path=[CONF_WAKEUP_PIN_MODE]
)
multiple_pins = wakeup_pins is not None and len(wakeup_pins) > 1
# wakeup_pin is limited to ESP32 and BK72XX, and only BK72XX accepts a list
if not CORE.is_bk72xx:
@@ -356,6 +352,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_NRF52]),
cv.key_requires(CONF_WAKEUP_PIN_MODE, CONF_WAKEUP_PIN),
validate_config,
)
+3 -3
View File
@@ -96,9 +96,9 @@ static void __attribute__((flatten)) draw_packed_pixels(Display *display, int x_
// Remove before 2027.5.0. Draws a block in the deprecated BGR or GRB order, pixel by pixel. Not inlined, so
// the RGB loops above compile as tightly as before.
static void __attribute__((noinline))
draw_ordered_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
static void __attribute__((noinline)) draw_ordered_pixels(Display *display, int x_start, int y_start, int w, int h,
const uint8_t *ptr, ColorOrder order, ColorBitness bitness,
bool big_endian, int x_offset, int y_offset, int x_pad) {
const size_t line_stride = x_offset + w + x_pad;
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
+5 -2
View File
@@ -198,7 +198,8 @@ template<typename T> class DisplayWriter {
// For stateless lambdas (convertible to function pointer): use function pointer (4 bytes)
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> && std::convertible_to<F, void (*)(T &)>
DisplayWriter(F f)
requires std::invocable<F, T &> && std::convertible_to<F, void (*)(T &)>
: type_(STATELESS_LAMBDA) {
this->stateless_f_ = f; // Implicit conversion to function pointer
}
@@ -206,7 +207,9 @@ template<typename T> class DisplayWriter {
// For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated)
// This handles backwards compatibility with external components
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> &&(!std::convertible_to<F, void (*)(T &)>) : type_(LAMBDA) {
DisplayWriter(F f)
requires std::invocable<F, T &> && (!std::convertible_to<F, void (*)(T &)>)
: type_(LAMBDA) {
this->f_ = new std::function<void(T &)>(std::move(f));
}
@@ -59,10 +59,10 @@ class DisplayMenuComponent : public Component {
update();
}
virtual void on_before_show(){};
virtual void on_after_show(){};
virtual void on_before_hide(){};
virtual void on_after_hide(){};
virtual void on_before_show() {};
virtual void on_after_show() {};
virtual void on_before_hide() {};
virtual void on_after_hide() {};
uint8_t rows_;
bool active_;
@@ -82,7 +82,7 @@ std::string MenuItemNumber::get_value_text() const {
result = this->value_getter_.value()(this);
} else {
char data[32];
snprintf(data, sizeof(data), this->format_.c_str(), get_number_value_());
snprintf(data, sizeof(data), this->format_, get_number_value_());
result = data;
}
@@ -118,7 +118,8 @@ class MenuItemNumber final : public MenuItemEditable {
public:
explicit MenuItemNumber() : MenuItemEditable(MENU_ITEM_NUMBER) {}
void set_number_variable(number::Number *var) { this->number_var_ = var; }
void set_format(const std::string &fmt) { this->format_ = fmt; }
// Keeps the pointer; the string must outlive the item (codegen passes a literal)
void set_format(const char *fmt) { this->format_ = fmt; }
bool has_value() const override { return true; }
std::string get_value_text() const override;
@@ -130,7 +131,7 @@ class MenuItemNumber final : public MenuItemEditable {
float get_number_value_() const;
number::Number *number_var_{nullptr};
std::string format_;
const char *format_{""};
};
#endif
@@ -139,8 +140,9 @@ class MenuItemSwitch final : public MenuItemEditable {
public:
explicit MenuItemSwitch() : MenuItemEditable(MENU_ITEM_SWITCH) {}
void set_switch_variable(switch_::Switch *var) { this->switch_var_ = var; }
void set_on_text(const std::string &t) { this->switch_on_text_ = t; }
void set_off_text(const std::string &t) { this->switch_off_text_ = t; }
// Both keep the pointer; the strings must outlive the item (codegen passes literals)
void set_on_text(const char *t) { this->switch_on_text_ = t; }
void set_off_text(const char *t) { this->switch_off_text_ = t; }
bool has_value() const override { return true; }
std::string get_value_text() const override;
@@ -153,8 +155,8 @@ class MenuItemSwitch final : public MenuItemEditable {
bool toggle_switch_();
switch_::Switch *switch_var_{nullptr};
std::string switch_on_text_;
std::string switch_off_text_;
const char *switch_on_text_{""};
const char *switch_off_text_{""};
};
#endif
+2 -1
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import socket
from esphome.components import network, socket
from esphome.components.light.effects import register_addressable_effect
from esphome.components.light.types import AddressableLightEffect
import esphome.config_validation as cv
@@ -38,6 +38,7 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
network.require_ipv4("e131"),
socket.consume_sockets(1, "e131", socket.SocketType.UDP),
)
+37
View File
@@ -1,2 +1,39 @@
from collections.abc import Iterable
from dataclasses import dataclass, field
from pathlib import Path
from esphome.core import CORE
CODEOWNERS = ["@esphome/core"]
DOMAIN = "epaper_spi"
# The source file that every model needs.
BASE_SOURCE_FILE = "epaper_spi.cpp"
@dataclass
class EpaperSpiData:
needed_sources: set[str] = field(default_factory=set)
def _get_data() -> EpaperSpiData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = EpaperSpiData()
return CORE.data[DOMAIN]
def register_sources(sources: Iterable[str]) -> None:
"""Record the source files that a configured model's C++ driver class needs."""
_get_data().needed_sources.update(sources)
def FILTER_SOURCE_FILES() -> list[str]: # noqa: N802
"""Exclude the driver sources that no configured model needs."""
if not (needed := _get_data().needed_sources):
return []
keep = needed | {BASE_SOURCE_FILE}
return sorted(
path.name
for path in Path(__file__).parent.glob("*.cpp")
if path.name not in keep
)
+6 -1
View File
@@ -42,7 +42,11 @@ from esphome.const import (
)
from esphome.cpp_generator import RawExpression
from . import models
from . import ( # pylint: disable=unused-import
FILTER_SOURCE_FILES, # noqa: F401
models,
register_sources,
)
AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"]
@@ -219,6 +223,7 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
model = MODELS[config[CONF_MODEL]]
register_sources(model.source_files)
init_sequence = config.get(CONF_INIT_SEQUENCE)
if init_sequence is None:
@@ -22,11 +22,11 @@ void EPaperMono::deep_sleep() {
}
bool EPaperMono::reset() {
if (EPaperBase::reset()) {
if (!EPaperBase::reset())
return false;
if (this->software_reset_)
this->command(0x12);
return true;
}
return false;
return true;
}
void EPaperMono::set_window() {
@@ -15,12 +15,13 @@ class EPaperMono : public EPaperBase {
protected:
void refresh_screen(bool partial) override;
void power_on() override {}
void power_off() override{};
void power_off() override {};
void deep_sleep() override;
bool reset() override;
virtual void set_window();
bool transfer_data() override;
bool send_red_{true};
bool software_reset_{true}; // send 0x12 after the hardware reset
// When set, the red RAM pass writes the image rather than blank data.
bool send_red_as_image_{false};
};
@@ -0,0 +1,35 @@
#include "epaper_spi_ssd1681.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1681");
bool HOT EPaperSSD1681::transfer_data() {
if (this->current_data_index_ == 0 && this->send_red_) {
if (!this->partial_) {
// A refresh drives the whole panel from RAM, so a full one makes both RAM banks whole again
this->x_low_ = 0;
this->x_high_ = this->width_;
this->y_low_ = 0;
this->y_high_ = this->height_;
} else {
// 0x26 already holds the image on the panel: the controller copies 0x24 into it after each refresh
this->set_window();
this->send_red_ = false;
}
}
return EPaperSSD1683::transfer_data();
}
void EPaperSSD1681::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->cmd_data(0x3C, {partial ? (uint8_t) 0x80 : (uint8_t) 0x01});
// 0x26 is compared as it is on a partial refresh and ignored on a full one
this->cmd_data(0x21, {partial ? (uint8_t) 0x00 : (uint8_t) 0x40, (uint8_t) 0x00});
// Both end with analog and OSC off before the panel goes to sleep
this->cmd_data(0x22, {partial ? (uint8_t) 0xFF : (uint8_t) 0xF7});
this->command(0x20);
}
} // namespace esphome::epaper_spi
@@ -0,0 +1,23 @@
#pragma once
#include "epaper_spi_ssd1683.h"
namespace esphome::epaper_spi {
// Partial refreshes compare RAM 0x24 (new image) with RAM 0x26 (image on the panel) and drive only changed pixels.
class EPaperSSD1681 : public EPaperSSD1683 {
public:
EPaperSSD1681(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperSSD1683(name, width, height, init_sequence, init_sequence_length) {
// 0x12 puts the display option register back to its default, which turns off the RAM ping-pong the panel's
// OTP enables; without it the controller no longer carries 0x24 over to 0x26 after a partial refresh
this->software_reset_ = false;
}
protected:
bool transfer_data() override;
void refresh_screen(bool partial) override;
};
} // namespace esphome::epaper_spi
@@ -18,17 +18,24 @@ class EpaperModel:
# Whether the driver manages chip-select itself instead of via the SPI bus.
manages_cs: bool = False
# The .cpp files this model's driver class needs: its own and those of every parent class
# except epaper_spi.cpp, which is always built. Subclasses override this next to their class_name.
source_files: tuple[str, ...] = ()
def __init__(
self,
name: str,
class_name: str,
initsequence=(),
source_files: tuple[str, ...] | None = None,
**defaults,
):
name = name.upper()
self.name = name
self.class_name = class_name
self.initsequence = initsequence
if source_files is not None:
self.source_files = source_files
self.defaults = defaults
EpaperModel.models[name] = self
@@ -112,7 +119,13 @@ class EpaperModel:
initsequence.extend(kwargs.pop("add_init_sequence", ()))
defaults = self.defaults.copy()
defaults.update(kwargs)
return self.__class__(name, initsequence=tuple(initsequence), **defaults)
model = self.__class__(name, initsequence=tuple(initsequence), **defaults)
# Subclass constructors do not all take these, so carry them over unless overridden
if "class_name" not in kwargs:
model.class_name = self.class_name
if "source_files" not in kwargs:
model.source_files = self.source_files
return model
def check_requirements(self) -> None:
"""
@@ -4,6 +4,8 @@ from . import EpaperModel
class Inkplate2Model(EpaperModel):
source_files = ("epaper_spi_inkplate2.cpp",)
def __init__(self, name, class_name="EPaperInkplate2", **kwargs):
super().__init__(name, class_name, **kwargs)
@@ -6,6 +6,11 @@ from . import EpaperModel
class Inkplate6ColorModel(EpaperModel):
source_files = (
"epaper_spi_4bpp.cpp",
"epaper_spi_inkplate6color.cpp",
)
def __init__(self, name, class_name="EPaperInkplate6Color", **kwargs):
super().__init__(name, class_name, **kwargs)
@@ -9,6 +9,8 @@ from . import EpaperModel
class JD79660(EpaperModel):
source_files = ("epaper_spi_jd79660.cpp",)
def __init__(self, name, class_name="EPaperJD79660", fast_update=None, **kwargs):
# Only a fast_update sequence lets the driver do anything but a full refresh
kwargs.setdefault("partial_update", fast_update is not None)
@@ -4,6 +4,11 @@ from . import EpaperModel
class SpectraE6(EpaperModel):
source_files = (
"epaper_spi_4bpp.cpp",
"epaper_spi_spectra_e6.cpp",
)
def __init__(self, name, class_name="EPaperSpectraE6", **defaults):
super().__init__(name, class_name, **defaults)
@@ -14,6 +14,11 @@ MONOCHROME = "monochrome"
class SSD1677(EpaperModel):
source_files = (
"epaper_spi_mono.cpp",
"epaper_spi_ssd1677.cpp",
)
def __init__(
self,
name: str,
@@ -116,6 +121,11 @@ seeed_sticky = ssd1677.extend(
seeed_sticky.extend(
"seeed-reterminal-sticky-gray4",
class_name="EPaperSSD1677Gray4",
source_files=(
"epaper_spi_mono.cpp",
"epaper_spi_ssd1677.cpp",
"epaper_spi_ssd1677_gray4.cpp",
),
border_waveform=0x00,
partial_update=MONOCHROME,
# each plane byte is built from two whole buffer bytes
@@ -0,0 +1,42 @@
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_DIMENSIONS, CONF_RESET_PIN
from esphome.types import ConfigType
from .ssd1683 import SSD1683
MAX_WIDTH = 200
MAX_HEIGHT = 200
class SSD1681(SSD1683):
source_files = (
"epaper_spi_mono.cpp",
"epaper_spi_ssd1681.cpp",
"epaper_spi_ssd1683.cpp",
)
def __init__(
self, name: str, class_name: str = "EPaperSSD1681", **defaults: Any
) -> None:
super().__init__(name, class_name=class_name, **defaults)
def validate_config(self, config: ConfigType) -> ConfigType:
config = super().validate_config(config)
width, height = self.get_dimensions(config)
if width > MAX_WIDTH or height > MAX_HEIGHT:
raise cv.Invalid(
f"{self.name} supports at most {MAX_WIDTH}x{MAX_HEIGHT} pixels",
path=[CONF_DIMENSIONS],
)
if CONF_RESET_PIN not in config:
# The panel sleeps after every update, and only the hardware reset wakes it
raise cv.Invalid(f"{self.name} needs a {CONF_RESET_PIN}")
return config
ssd1681 = SSD1681("ssd1681", width=200, height=200)
ssd1681.extend("goodisplay-gdey0154d67-1.54")
ssd1681.extend("waveshare-1.54in-v2")
@@ -4,6 +4,11 @@ from . import EpaperModel
class SSD1683(EpaperModel):
source_files = (
"epaper_spi_mono.cpp",
"epaper_spi_ssd1683.cpp",
)
def __init__(self, name, class_name="EPaperSSD1683", data_rate="20MHz", **defaults):
defaults[CONF_DATA_RATE] = data_rate
defaults.setdefault("partial_update", True)
@@ -26,15 +31,3 @@ goodisplay_gdey042t81 = ssd1683.extend(
width=400,
height=300,
)
# The SSD1681 shares the SSD1683 command set for everything this driver uses:
# the same 0x01/0x11/0x18 initialisation, the same 0x44/0x45 window and
# 0x4E/0x4F cursor registers with a byte-addressed X and a 16-bit Y, and the
# same 0x22/0x20 update. It only differs in the panel sizes it drives.
ssd1681 = SSD1683("ssd1681")
waveshare_1_54in_v2 = ssd1681.extend(
"waveshare-1.54in-v2",
width=200,
height=200,
)
@@ -22,6 +22,8 @@ CONF_CS1_PIN = "cs1_pin"
class T133A01Model(EpaperModel):
"""EpaperModel subclass for T133A01-based 6-color e-paper displays."""
source_files = ("epaper_spi_t133a01.cpp",)
# The driver drives CS and CS1 directly for the dual-CS protocol.
manages_cs = True
@@ -22,6 +22,8 @@ MAX_GATES = 296
class UC8151(EpaperModel):
"""EpaperModel class for black/white/red displays using the UC8151D controller."""
source_files = ("epaper_waveshare_bwr.cpp",)
def __init__(self, name: str, **defaults: Any) -> None:
super().__init__(name, "EPaperWaveshareBWR", **defaults)
@@ -24,6 +24,11 @@ from . import EpaperModel, ultrachip_resolution
class UC8179(EpaperModel):
"""EpaperModel class for monochrome displays using the UC8179 controller."""
source_files = (
"epaper_spi_ultrachip.cpp",
"epaper_spi_uc8179.cpp",
)
def __init__(
self,
name: str,
@@ -19,6 +19,11 @@ from . import EpaperModel, ultrachip_resolution
class UC8279(EpaperModel):
source_files = (
"epaper_spi_ultrachip.cpp",
"epaper_spi_uc8279.cpp",
)
def __init__(
self,
name: str,
@@ -11,6 +11,11 @@ from . import EpaperModel
class WaveshareModel(EpaperModel):
source_files = (
"epaper_spi_mono.cpp",
"epaper_waveshare.cpp",
)
def __init__(
self,
name: str,
@@ -2,6 +2,11 @@ from . import EpaperModel
class WaveshareB(EpaperModel):
source_files = (
"epaper_waveshare_b.cpp",
"epaper_weact_3c.cpp",
)
def __init__(self, name, **defaults):
super().__init__(name, "EpaperWaveshareB", **defaults)

Some files were not shown because too many files have changed in this diff Show More