Compare commits

...
Author SHA1 Message Date
J. Nick Koston cfe402366c Use assert_device_name in the a-l integration tests 2026-10-20 05:29:33 -10:00
J. Nick Koston c0cbf9cdfd [tests] Add require_service, assert_device_name and exact entity lookups (#21150) 2026-10-20 09:07:31 -04:00
J. Nick Koston 89eb58edce [ld2420][ld2450] Use Apply numbers for hub forwarding numbers (#21146) 2026-10-20 09:06:58 -04:00
J. Nick Koston 442bd4975e [ci] Add the SEN0395, LD2450 zone and held light report flows to the hardware-test skill (#21160) 2026-10-20 09:05:05 -04:00
J. Nick KostonandJonathan Swoboda 808075bf93 [components] Set optional plain values with cg.add_optional (n-z) (#21183)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-20 09:03:51 -04:00
J. Nick Koston a2fbedeac3 [components] Set optional plain values with cg.add_optional (a-m) (#21181) 2026-10-20 08:55:50 -04:00
J. Nick Koston 8b951fa620 [haier_tundra][tm1638][bl0940][micronova] Use Apply entities for hub forwarding buttons and switches (#21148) 2026-10-20 08:52:26 -04:00
J. Nick Koston b18a5b09e7 [tests] Drop the external component path replacements the yaml_config fixture already does (#21149) 2026-10-20 08:50:38 -04:00
J. Nick Koston 6c4a42c069 [daikin][daikin_brc][daikin_arc][whirlpool][samsung] Send and read IR frame bytes with the remote_base helpers (#21170) 2026-10-20 08:45:43 -04:00
J. Nick Koston 18120cc4a8 [airton][ballu][delonghi][hitachi_ac424][mitsubishi] Send and read IR frame bytes with the remote_base helpers (#21169) 2026-10-20 08:45:03 -04:00
J. Nick Koston 7719179dcd [tcl112][zhlt01][heatpumpir][fujitsu_general] Send and read IR frame bytes with the remote_base helpers (#21168) 2026-10-20 08:44:13 -04:00
J. Nick Koston 74e1057268 [yashima][remote_base] Send IR frame bytes with item_bytes, including most significant bit first (#21171) 2026-10-20 08:39:49 -04:00
J. Nick Koston 6304411341 [core] Add cg.add_optional for optional plain config values (#21180) 2026-10-20 08:38:02 -04:00
J. Nick Koston fd902a646a [ota] Accept an unknown image size in the RP2040 backend so web_server OTA works (#21178) 2026-10-20 07:09:13 -04:00
John McLear 09b580e4ea [st25r] Add ST25R3916 NFC reader (SPI, ISO14443A) (#15608) 2026-10-19 22:53:50 -10:00
esphome[bot] 89536632dc Bump bundled esphome-device-builder to 1.25.0 (#21175) 2026-10-19 20:18:55 -10:00
Clyde Stubbs 3da581233a [st7305] Fix position of partial updates on landscape panels (#21172) 2026-10-20 16:08:03 +11:00
J. Nick Koston ff1c448ecc [core] Document the new codegen, bus and Apply entity helpers in AGENTS.md (#21130) 2026-10-19 17:35:09 -10:00
J. Nick Koston 1430428f70 [tests] Use wait_or_fail in the api, light, script and other integration tests (#21165) 2026-10-19 17:34:25 -10:00
J. Nick Koston 91f7b0d147 [tests] Use wait_or_fail in the sensor, text, loop and host integration tests (#21164) 2026-10-19 17:34:00 -10:00
J. Nick Koston 5bccdab01d [remote_base] Add helpers to send and read IR frame bytes bit by bit (#21167) 2026-10-19 17:29:11 -10:00
Jesse Hills 68df9248ed [sx126x][sx127x] Move CONF_RST_PIN to the shared component constants (#21174) 2026-10-20 16:11:27 +13:00
J. Nick Koston 0a501b7ef5 [core] Pass the LOG_PIN prefix to the log straight from flash (#21117) 2026-10-19 20:04:39 -04:00
J. Nick Koston b65395174c [core] Log the build time at boot straight from flash (#21118) 2026-10-19 20:04:05 -04:00
J. Nick Koston da0fed0517 [dfrobot_sen0609][airton] Use Apply selects and switches for hub forwarding entities (#21147) 2026-10-19 14:02:53 -10:00
J. Nick Koston 984ecc9f3c [tests] Add wait_or_fail for integration test timeouts (#21152) 2026-10-19 14:02:19 -10:00
J. Nick Koston 7a1ed1ce34 [atm90e32] Use Apply buttons for the calibration buttons (#21163) 2026-10-19 14:01:51 -10:00
J. Nick Koston c6eeeac7e8 [copy] Use an Apply button for the copy button (#21162) 2026-10-19 14:01:26 -10:00
Igor Stadnyk 01524a2ffd [ld2410] Wait for complete query parameter acknowledgements (#21145) 2026-10-19 11:29:39 -10:00
dependabot[bot] 65ba64b9a6 Bump ruff from 0.17.0 to 0.17.1 (#21137) 2026-10-19 09:59:49 -10:00
dependabot[bot] 1b6a43a8a1 Bump esphome/workflows/.github/workflows/lock.yml from 2026.9.0 to 2026.10.2 (#21142) 2026-10-19 09:59:35 -10:00
Jonathan Swoboda 7a88c73fac [espota2] Stop the fake OTA device once the client is done (#21134) 2026-10-19 15:36:22 -04:00
dependabot[bot] e3a7437bdd Bump CodSpeedHQ/action from 5.4.2 to 5.5.0 (#21143)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 09:34:30 -10:00
dependabot[bot] ee53f8ab1c Bump platformdirs from 4.13.0 to 4.13.3 (#21136)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 09:33:07 -10:00
dependabot[bot] bc8bebfaba Bump filelock from 4.1.1 to 4.2.0 (#21138)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 19:33:00 +00:00
dependabot[bot] 0ab57126d8 Bump github/codeql-action/analyze from 4.38.3 to 4.39.0 (#21139)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 09:32:54 -10:00
dependabot[bot] 6d8e36cc97 Bump esphome/workflows/.github/workflows/stale.yml from 4fbce13026c393d6eb6b247e394dcd9ef1af76c2 to 31d160e5f0ffa0ee65d5b27cb55c153d415f2abe (#21141)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 09:32:46 -10:00
dependabot[bot] 0b70f297c2 Bump github/codeql-action/init from 4.38.3 to 4.39.0 (#21140)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 09:32:35 -10:00
dependabot[bot] 89a222b95a Bump esptool from 5.5.0 to 5.6.0 (#21135)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 09:32:21 -10:00
Daeho RoandJonathan Swoboda b537611bfa [core] Support Python 3.15 (#21052)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-19 09:15:54 -10:00
a3d3dd4a51 [combination] Remove @Cat-Ion from codeowners (#21133)
Co-authored-by: Lynn Ochs <lynn@gremlynn.de>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-19 18:48:53 +00:00
Igor Stadnyk a5fbd34a1e [ld2460] Preserve buffered frames across overflow (#21123) 2026-10-19 08:23:31 -10:00
Igor Stadnyk fd3c1fad45 [ld2410s] Fix parameter writes after long uptime (#21121) 2026-10-19 08:23:07 -10:00
Igor Stadnyk f250fa73d0 [ld2420] Resume UART reads after a silent command timeout (#21131) 2026-10-19 08:22:32 -10:00
J. Nick Koston 85dd0f71b3 [core] Add Apply entities so hub forwarding buttons, switches, numbers and selects need no class (#20935) 2026-10-19 07:50:56 -10:00
J. Nick Koston 7cf72c287d [web_server] Pass the listing flag as a bool and share the state event send (#21112) 2026-10-19 07:50:39 -10:00
J. Nick Koston dc56b27f33 [api] Send the device info strings from flash and keep the version out of RAM (#21106) 2026-10-19 07:50:21 -10:00
J. Nick Koston 384c2ec26c [ci] Extend the hardware-test skill with the audio, globals, outgoing connection and Home Assistant light flows (#21104) 2026-10-19 07:49:34 -10:00
Jonathan Swoboda 6868011336 [combination][speaker_source][const] Share CONF_SOURCES from components/const (#21129) 2026-10-19 07:49:05 -10:00
J. Nick Koston 8ffdbfd4ce [homeassistant] Judge a light report against the transition it arrived during (#21100) 2026-10-19 07:48:32 -10:00
J. Nick Koston 1cd5641900 [web_server] Keep the remaining ESP8266 web server strings in flash (#21113) 2026-10-19 12:25:30 -04:00
Clyde Stubbs 15b988be9c [cst9220] Fix long press reported as several short presses (#21034) 2026-10-19 20:44:22 +11:00
Clyde Stubbs 2d0e3faa3d [st7305] Add reflective LCD display driver (#21099) 2026-10-19 19:57:05 +11:00
esphome[bot] 2087d28684 Bump aioesphomeapi from 46.12.0 to 46.14.0 (#21114) 2026-10-19 08:07:50 +00:00
b919b692f7 [scd4x] Fix ambient_pressure_compensation unit handling (#19888)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-18 21:54:33 -10:00
Clyde Stubbs 4498ad2c77 [core] Warn when the .esphome build tree is tracked by git (#18973) 2026-10-19 18:38:47 +11:00
esphome[bot] 2addbaaa7f [ci] Refresh integration test durations (#21105) 2026-10-18 21:31:16 -10:00
1dc4006bfc [usb_audio] Add USB Audio Class (UAC) host support (#11728)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-18 17:44:59 -10:00
409 changed files with 10044 additions and 3249 deletions
+10 -4
View File
@@ -93,13 +93,15 @@ ESP-IDF OTA image: `.esphome/build/<name>/build/firmware.ota.bin`. LibreTiny OTA
- `ports.py`: list ports with VID:PID, product, serial and a board guess; `--wait VID[:PID]`,
`--product NAME`, `--serial SN` (one specific board, whatever path it comes back under) and
`--serial-of PORT` (the serial number behind a path).
- `serial_capture.py PORT SECONDS OUT [--baud] [--released] [--reset] [--append]`: bounded timestamped
capture, ANSI stripped, `LOG_SECRET` text written as `<secret>`, reopens a vanished port, exits at the
- `serial_capture.py PORT SECONDS OUT [--baud] [--released] [--reset] [--append] [--ms]`: bounded timestamped
capture (`--ms` adds milliseconds for timing work), ANSI stripped, `LOG_SECRET` text written as `<secret>`, reopens a vanished port, exits at the
deadline. OUT is truncated unless `--append` is given, so counts over it cover one run.
- `flash_rtl87xx_kit.py`: ltchiptool wrapper for the BW15 kit whose CH340 drives CEN (RTS) and the boot
strap (DTR). `flash read` needs the family `realtek-ambz2`; `flash write` does not.
- `flash_bk72xx.py`: ltchiptool wrapper for BK72xx boards whose RTS drives CEN. It misses the link now
and then; retry up to three times.
- `api_button.py HOST KEY list|press NAME [...]`: list a device's template buttons or press them over the API; the
recipes drive their test configs with it.
- `wifisave_hammer.sh KIND DUT_PORT DUT_YAML DUT_TREE CLIENT_PORT CLIENT_YAML CLIENT_TREE [SETTLE]`:
the fallback AP plus captive portal hammer end to end (compile, flash, capture, counts) with the
templates below. KIND is `esp`, `pico`, `bw15` or `bk`. It ends with `RESULT PASS` (exit 0),
@@ -113,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).
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).
## Templates in `$SKILL/templates`
@@ -125,7 +127,11 @@ capture decoder).
- Per-topic device configs, each described in `reference/recipes/<component>.md`: `online-image-dut.yaml`,
`usb-cdc-acm-dongle-dut.yaml`, `nrf52-preferences-dut.yaml`, `rtsp-waveshare-s3-eth-dut.yaml`,
`waveshare-s3-eth-camdiag.yaml`, `s31-cse7766-dut.yaml`, `nrf52-coredump-crash-dut.yaml`,
`bk72xx-rtc-prefs-dut.yaml`, `bk72xx-crash-probe-dut.yaml`, `dut-ap-only-wipe-once-bk72xx.yaml`.
`bk72xx-rtc-prefs-dut.yaml`, `bk72xx-crash-probe-dut.yaml`, `dut-ap-only-wipe-once-bk72xx.yaml`,
`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`.
## When something looks wrong
@@ -4,12 +4,12 @@ Each flow names the pull requests it was used on, so their result tables show wh
One file per component, named after it:
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266, outgoing connection wake timing on deep sleep and after a Wi-Fi drop
- [`captive_portal.md`](captive_portal.md): fallback AP and /wifisave hammer
- [`cse7766.md`](cse7766.md): Sonoff S31 parser check over the API
- [`espnow.md`](espnow.md): ESP8266 broadcast receive
- [`hdmi_cec.md`](hdmi_cec.md): two-board wire
- [`ld2450.md`](ld2450.md): polygon zones
- [`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
- [`nrf52.md`](nrf52.md): dongle board support
@@ -24,3 +24,7 @@ One file per component, named after it:
- [`zephyr.md`](zephyr.md): preferences on the nRF52840 dongle
- [`zephyr_coredump.md`](zephyr_coredump.md): crash and decode on nRF52 boards
- [`zwave_proxy.md`](zwave_proxy.md): frame checksums on a live proxy
- [`globals.md`](globals.md): restoring globals with storage rtc and flash across every reset kind
- [`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
@@ -5,3 +5,26 @@
Host side timing is useless on a weak link; use an on-device bench with the minimum of five runs over
two boots. Provision a key with `noise_encryption_set_key` through aioesphomeapi and upload with
`espota2.run_ota(host, 8266, None, bin, noise_psk=key)` when the key is not in the yaml.
## Outgoing connection wake timing on a deep sleep build (#21077)
`templates/deep-sleep-outgoing-dut.yaml`: classic ESP32 ESP-IDF, `wifi: fast_connect`, `api: encryption` with a bare
`outgoing_connection:`, `deep_sleep` 15 s run and 15 s sleep, a `homeassistant` text sensor, and `millis()` logged
from `wifi: on_connect`, `api: on_client_connected` and the sensor's first `on_value`. Home Assistant's dial-in
listener is on 6054; reuse a device name it already has. Flash by USB, `serial_capture.py PORT 360 OUT --reset` for
12 cycles, then `scripts/api_outgoing/cycles.py OUT LABEL` prints per cycle the wifi, client and state millis with
the client-wifi and state-client gaps. Wi-Fi takes 4 to 8 s after a wake, so compare the client-wifi gap, and run
the PR twice around one base run because the spread is a few hundred ms.
Results: https://github.com/esphome/esphome/pull/21077#issuecomment-6254098014
## Outgoing connection on a mains build after a Wi-Fi drop (#21085)
`templates/outgoing-wifi-drop-dut.yaml`: classic ESP32, `outgoing_connection: delay: 2s`, a "Wifi cycle" button doing
`wifi.disable`, 4 s, `wifi.enable`, a `debug` loop-time sensor, millis from the wifi and api triggers. Flash by USB,
`serial_capture.py PORT SECONDS OUT --ms`, press the button N times 40 s apart with `api_button.py DEV_IP KEY press
"Wifi cycle"`, then `wificycles.py CAPTURE LABEL` prints per drop the Wi-Fi-to-dial gap, dials and dials closed, who
connected first, and the loop-time maxima. For the loop cost of polling, repeat with an unreachable
`host: 192.0.2.1`. Home Assistant refuses a dial-in while its own session is READY (`Refusing dial-in adoption`),
so after a drop every dial is closed and HA's incoming reconnect wins 11 to 33 s later; the measurable effect is the
first dial landing in the loop the network flag flips.
Results: https://github.com/esphome/esphome/pull/21085#issuecomment-6254742791
@@ -0,0 +1,21 @@
# dfrobot_sen0395
## DFRobot SEN0395 mmWave on an ESP32 DevKit V1 (#20906, #20979)
Wiring: V to VIN (5 V, the module wants 3.6 to 5 V), G to GND, sensor TX to GPIO16 (RX2), sensor RX to GPIO17
(TX2), IO2 to GPIO18 (presence output, a gpio binary sensor), I1 unconnected. `templates/dfrobot-sen0395-dut.yaml`
has the hub, both detection binary sensors, the four switches, settings buttons and junk-line buttons.
`scripts/dfrobot_sen0395/ctl.py HOST KEY press:NAME sw:NAME=on|off wait:S` drives it and prints binary sensor and
switch updates; `cases.sh DEV_IP KEY CAPTURE LABEL` runs the whole flow against a running serial capture. Flash by
USB, `serial_capture.py PORT 540 CAPTURE --reset`, then the cases; compare the `Sending command` and `Message:` lines
and the counts of `Done`, `Error`, prompts and frames between builds. The module echoes every command, so the echoed
lines are the bytes the component sent and can be compared between builds as a set.
Module facts: it answers `Done` or `Error` and then the `leapMMW:/>` prompt without a line end; a junk line gets
`<line> is not recognized as a CLI command` (36 chars longer than the line, so 220 and 240 byte junk gives 256 and
276 char replies); input lines of 256 bytes or more make the module assert (`..\User_code\main.c line:232`) and
restart, after which the sensor is stopped; a factory reset restarts the module about 7 s later; `sensorStop` on a
stopped sensor answers `sensor stopped already` then `Error`; a settings press takes 5 to 6 s (stop, change, save,
start). Free heap on the ESP32 is Wi-Fi noise (192 to 198 kB) and says nothing about the component.
Results: https://github.com/esphome/esphome/pull/20979#issuecomment-6279129146 and
https://github.com/esphome/esphome/pull/20906#issuecomment-6279129604
@@ -0,0 +1,15 @@
# globals
## Restoring globals with storage: rtc and flash (#13954)
`templates/globals-storage-dut.yaml` is the shared package: five restoring globals (rtc int, flash int, default int,
rtc `std::string`, rtc `uint8_t[2]`), bumped and logged with the reset reason on every boot, saved and synced after
2 s, odd boots `App.safe_reboot()`. Platform files add the board (`packages: dut: !include globals-storage-dut.yaml`), the `debug` reset-reason text sensor and, on the
ESP32, `deep_sleep: run_duration: 12s, sleep_duration: 8s` (even boots sleep). No network needed: flash by USB,
`serial_capture.py PORT 110 OUT --reset` gives the reset pulse plus a run of soft restarts and sleeps; the ESP32's
EN pulse is a `POWERON_RESET` that empties RTC memory, the D1 mini's RST pulse (`rst cause:2`) keeps it, so the
ESP8266 needs a real power cut by hand; arm long `--released` captures on both ports first (the capture
reopens the port when it comes back, the very first boot lines are still lost, read the next boot's restored
values). Unchanged-build check: the config with every `storage:` removed, built on the PR and on the dev merge
commit under the PR's own commits (not the merge base, which is older dev), `cmp -l` plus an `nm -S` symbol diff;
only the template names differ. Results: https://github.com/esphome/esphome/pull/13954#issuecomment-6254794712
@@ -0,0 +1,47 @@
# homeassistant
## homeassistant light platform against a local Home Assistant (#11354)
A local Home Assistant dev install (`hass -c config`, https://127.0.0.1:8123 with a self-signed cert); its ESPHome
entries and entity ids are in `config/.storage/`. Two ESPHome RGBCW bulbs with a plaintext API are the lights, so an
"HA side" change is made directly on a bulb with `scripts/homeassistant_light/bulb.py BULB_IP - on|off|state|restart`.
Add the test devices to HA by hand with the shared `api_key` and "Allow the device to perform Home Assistant actions".
`templates/homeassistant-light-dut.yaml` is a package: two mirrors, buttons for each change, a "Log state" button, a
30 s alive line, an `on_boot` `light.turn_on` for the change-before-first-write path, and VERBOSE logging with the
noisy tags at DEBUG (a tag cannot exceed the global level). A platform file sets the board and includes it:
```yaml
esphome:
name: ha-light-dut
esp32:
board: esp32dev
framework:
type: esp-idf
packages:
dut: !include homeassistant-light-dut.yaml
```
Flash by USB, start a bounded `esphome logs` capture per device, then press buttons with
`scripts/api_button.py DEV_IP KEY press "Button"` and read the bulb with `bulb.py`. Cases: each
button against the bulb and the device log; a direct bulb change that both mirrors follow; restart the bulb, send a
change, expect the 5 s `did not report the change back` and the fallback (with an off and with an on last report);
OTA reboot with a serial capture running to see the boot-time change (the API log client reconnects too late).
Lessons: the light core clamps mireds to the range HA reported; a flash sends nothing; a change sent while HA is
connected but not yet subscribed is dropped by the api server (`client has not subscribed to actions (yet)`); after a
fallback to an off report the mirror keeps the unsent brightness; a mirror that never saw its light on since boot has
no levels to restore. Results: https://github.com/esphome/esphome/pull/11354#issuecomment-6253630336 and
https://github.com/esphome/esphome/pull/11354#issuecomment-6254161409
## Held report versus a later call (#21144)
A report that arrives during a transition is held and applied when the transition ends, unless a call came after
it. `scripts/homeassistant_light/heldcase.py DEV_IP KEY BULB_IP same|many [BULB_KEY]` (a key of `-` is plaintext) keeps one API connection to the mirror
device and one to the bulb so the steps land inside a 3 s fade: press "A 50 fade 3s", 0.7 s later set the bulb to
90% directly (the report is held), 1.4 s in press the same fade again (or "A many calls", 16 calls in one loop
pass). Pass: the mirror and the bulb end at the second call's target. Only `same` reproduces the bug: a build
without the fix logs `Applying the report that arrived during the transition` and the bulb stays at 90%. `many`
ends at 100% on both builds because the last call's target differs from the held report, so it is a regression
check only, not a reproduction. Separate button presses are too slow (about 1.5 s each) to land inside the fade.
The mirrors are not API entities, so read their state from the "Log state" button's line. Results: https://github.com/esphome/esphome/pull/21144#issuecomment-6278257695
@@ -5,3 +5,11 @@
To make a target "leave" without anyone moving, set the radar's Zone-1 numbers over the target and
switch the zone type to `Filter` (the count drops to 0 within 0.5 s); set it back to `Disabled` after.
Writing those numbers blocks the ld2450 loop about 50 ms.
## Zone numbers round trip (#21146)
`scripts/ld2450/zonedrive.py HOST KEY(- for plaintext) set:NAME=VALUE sel:NAME=OPTION wait:S list` sets numbers and
selects over the API and prints every LD2450 state update. After each set the hub sends the set zone command, queries
the radar and republishes all twelve coordinates from the answer, so set each coordinate of each zone to a distinct
value and check the readbacks; no target is needed. Readbacks can arrive late over Wi-Fi, so match them by content.
Results: https://github.com/esphome/esphome/pull/21146#issuecomment-6278026922
@@ -0,0 +1,52 @@
# usb_audio
## USB audio on the Waveshare ESP32-S3-ETH (#21081, #11728)
`templates/usb-audio-waveshare-s3-eth-dut.yaml`: ESP-IDF, PoE, logs over the API on Ethernet (the native USB port is
the host port, so there is no console), `usb_audio` hub, a 48 kHz 16-bit microphone, a 48 kHz 24-bit speaker (many
speakerphones offer only 24-bit; a wrong format logs the device's alternates), buttons for capture start and stop,
a non-blocking beep fed from an 8 ms interval with `play(buf, len, 0)` (a blocking `play()` on the main loop trips
the task watchdog), and a status interval with mic bytes, speaker state and heap. Drive the buttons with
`scripts/api_button.py IP KEY press "Beep"`. Expect 192 kB/s microphone bytes and the FIFO split
RX 80 / NPTX 16 / PTX 104 in the build's `defines.h`; with the balanced split the 294-byte OUT pipe is refused
(`EP MPS (294) exceeds supported limit (128)`). A device whose OUT endpoint is larger than 600 bytes cannot be
claimed on an S3 at all.
## Music Assistant through sendspin
`templates/usb-audio-sendspin-s3-eth-dut.yaml` overlays the template above with the Apollo CAST-1 wiring: resampler
speakers into a mixer whose inputs never time out, `speaker_source` media player with the sendspin source, and
`sendspin: unpaired_access: true`. A `media_player: platform: speaker` is not a media-source consumer and gives
"Player over its buffer", no audio. Music Assistant finds the device by mDNS within 15 s of boot.
Read the result from the device, not only by ear: sendspin's sync task logs `Lost sync (N us off)` (DEBUG, tag
`SsSync`) for every correction over 5 ms, a positive N inserts that much silence, and each one is an audible click.
For the chunk lead histogram, edit the managed library in the build directory,
`.esphome/build/<name>/managed_components/sendspin__sendspin-cpp/src/sync_task.cpp` (an `SS_LOGD` before the
late-chunk skip in `decode_chunk()` printing the chunk's lead against `new_audio_client_playtime`), update only that
file's `size` and sha256 `hash` in the component's `CHECKSUMS.json`, and compile again; deleting `.component_hash` or
recomputing it makes the manager refuse or refetch. Check the edit is in the image with `strings firmware.ota.bin`.
A lead below -5 ms makes the library skip the whole chunk; an I2S speaker keeps it within 0.1 ms of zero, which is
the figure a USB speaker has to match.
Gotchas: wait two minutes after a reboot before the measured stream (safe mode's boot counter write stalls the cores
for about 140 ms once, 60 to 75 s after boot); with the serial console on, a DEBUG log line per report saturates
UART0 and trips the task watchdog, so use `logger: baud_rate: 0` and the API log; two overlapping `esphome logs`
captures double every count; and every audio test must include a pause and resume cycle, since a stream end that
leaves less than one transfer parked in the driver was what hung the speaker after #11728 merged.
## Controls
Apollo CAST PRO-1 (`templates/cast-pro-1-sendspin-control.yaml`): `Core.yaml` next to it is
`Integrations/ESPHome/Core.yaml` from ApolloAutomation/CAST_PRO-1, which also defines the `version` substitution.
Keep its framework log level at ERROR (DEBUG floods driver output and trips the watchdog); the package reboots itself
into Ethernet-only once it sees a link and appends the MAC suffix to the name. `wifi: enable_on_boot: false` keeps
sendspin off Wi-Fi.
Nabu Casa Connect AUX-2 (a pre-production unit): build from `packages/aux-2-pre-prod.yaml` at NabuCasa/aux-2 commit
283f2e64 (dropped in bde41e01), not the public `aux-2.yaml`, whose W5500, codec and I2S pins differ (the public one
reads W5500 chip ID 0x00 and fails the codec's I2C writes). Point its local `external_components` source at
`github://NabuCasa/aux-2@main`. Templates `aux-2-preprod-stock-dut.yaml` (I2S codec) and
`aux-2-preprod-usb-audio-dut.yaml` (the USB speaker as the mixer output via `!extend mixing_speaker`). The console is
the CH343 (1a86:55d3); first flash over it, OTA after. Forcing the Wi-Fi flag off with `restore_value: no` boot-loops
when Ethernet fails, so let the package switch itself.
@@ -0,0 +1,33 @@
"""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" [...]
"""
import asyncio
import sys
from aioesphomeapi import APIClient, ButtonInfo
HOST, KEY = sys.argv[1], sys.argv[2]
CMD, ARGS = sys.argv[3], sys.argv[4:]
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=KEY)
await c.connect(login=True)
ents, _ = await c.list_entities_services()
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
if CMD == "list":
print("\n".join(sorted(buttons)))
else:
for name in ARGS:
if name not in buttons:
sys.exit(f"no button named {name!r}; have {sorted(buttons)}")
c.button_command(buttons[name].key)
print("pressed", name)
await asyncio.sleep(0.3)
await asyncio.sleep(0.5)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,73 @@
"""Per wake cycle timings from a serial capture of the deep sleep timing config.
usage: cycles.py CAPTURE [LABEL]
A cycle starts at the boot banner; the numbers are the device's millis() from the timing lines, and the
capture clock gap from the first line of the cycle to the "Dialing" line.
"""
import pathlib
import re
import statistics
import sys
cap = (
pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").splitlines()
)
label = sys.argv[2] if len(sys.argv) > 2 else ""
TS = re.compile(r"^\[(\d\d):(\d\d):(\d\d)(?:\.(\d+))?\]")
def secs(line):
m = TS.match(line)
if not m:
return None
h, mi, s, f = m.groups()
return int(h) * 3600 + int(mi) * 60 + int(s) + (int(f) / 10 ** len(f) if f else 0)
cycles = []
cur = None
for line in cap:
if "rst:0x" in line or (cur is None and "Log initialized" in line):
cur = {"start": secs(line), "reset": line.split("]", 1)[-1].strip()[:60]}
cycles.append(cur)
continue
if cur is None:
continue
if "Dialing" in line:
cur.setdefault("dial_cap", secs(line))
m = re.search(r"wifi connected at (\d+) ms", line)
if m:
cur.setdefault("wifi", int(m.group(1)))
m = re.search(r"client connected at (\d+) ms \((.*?)\)", line)
if m and "client" not in cur:
cur["client"] = int(m.group(1))
cur["who"] = m.group(2)
m = re.search(r"first HA state '(.*?)' at (\d+) ms", line)
if m and "state" not in cur:
cur["state"] = int(m.group(2))
print(f"{label} cycles: {len(cycles)}")
print(
f"{'n':>2} {'wifi ms':>8} {'dial +ms':>9} {'client ms':>10} {'state ms':>9} {'client-wifi':>11} {'state-client':>12} client reset"
)
for i, c in enumerate(cycles, 1):
dial = ""
if c.get("dial_cap") is not None and c.get("start") is not None:
dial = f"{(c['dial_cap'] - c['start']) * 1000:.0f}"
cw = c["client"] - c["wifi"] if "client" in c and "wifi" in c else ""
sc = c["state"] - c["client"] if "state" in c and "client" in c else ""
print(
f"{i:>2} {c.get('wifi', ''):>8} {dial:>9} {c.get('client', ''):>10} {c.get('state', ''):>9} {cw:>11} {sc:>12} {c.get('who', '')} {c.get('reset', '')[:28]}"
)
for c in cycles:
if "client" in c and "wifi" in c:
c["client-wifi"] = c["client"] - c["wifi"]
if "state" in c and "client" in c:
c["state-client"] = c["state"] - c["client"]
for key in ("wifi", "client", "state", "client-wifi", "state-client"):
vals = [c[key] for c in cycles if key in c]
if vals:
print(
f"{key}: n={len(vals)} min={min(vals)} median={statistics.median(vals):.0f} max={max(vals)}"
)
@@ -0,0 +1,107 @@
"""Per Wi-Fi drop timings from a serial capture of the outgoing connection timing config.
usage: wificycles.py CAPTURE [LABEL]
A cycle starts at "wifi cycle disabling"; the numbers are the device's millis() from the timing lines.
"dial" says whether the client that connected first came from the device's own dial ("Dialing" seen after the
reconnect and before the hello) or from an incoming connection ("Accept" seen first).
"""
import pathlib
import re
import statistics
import sys
cap = (
pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").splitlines()
)
label = sys.argv[2] if len(sys.argv) > 2 else ""
TS = re.compile(r"^\[(\d\d):(\d\d):(\d\d)(?:\.(\d+))?\]")
def secs(line):
m = TS.match(line)
if not m:
return None
h, mi, se, f = m.groups()
return int(h) * 3600 + int(mi) * 60 + int(se) + (int(f) / 10 ** len(f) if f else 0)
cycles = []
cur = None
loop_max = []
for line in cap:
m = re.search(r"loop time max (\d+) ms", line)
if m:
loop_max.append(int(m.group(1)))
if "wifi cycle disabling" in line:
cur = {"order": []}
cycles.append(cur)
continue
if cur is None:
continue
m = re.search(r"wifi connected at (\d+) ms", line)
if m and "wifi" not in cur:
cur["wifi"] = int(m.group(1))
cur["wifi_cap"] = secs(line)
if "Dialing" in line:
cur["order"].append("dial")
cur["dials"] = cur.get("dials", 0) + 1
if "dial_cap" not in cur and "wifi_cap" in cur:
cur["dial_cap"] = secs(line)
elif "dial_cap" not in cur and "dial_before_wifi_cap" not in cur:
cur["dial_before_wifi_cap"] = secs(line)
if (
re.search(r"client disconnected at \d+ ms \(\d+\.\d+\.\d+\.\d+\)", line)
and "wifi" in cur
and "client" not in cur
):
cur["dial_closed"] = cur.get("dial_closed", 0) + 1
if "Accept " in line and "wifi" in cur:
cur["order"].append("accept")
m = re.search(r"client connected at (\d+) ms \((.*?)\)", line)
if m and "wifi" in cur and "client" not in cur:
cur["client"] = int(m.group(1))
cur["who"] = m.group(2).split(" ")[0]
cur["via"] = cur["order"][0] if cur["order"] else "?"
m = re.search(r"Connect (failed|timeout)", line)
if m:
cur.setdefault("connect_errors", 0)
cur["connect_errors"] += 1
print(f"{label} cycles: {len(cycles)}")
print(
f"{'n':>2} {'wifi ms':>8} {'dial-wifi cap ms':>16} {'client ms':>10} {'client-wifi':>11} via dials closed client"
)
for i, c in enumerate(cycles, 1):
cw = c["client"] - c["wifi"] if "client" in c and "wifi" in c else ""
c["client-wifi"] = cw
dw = ""
if "wifi_cap" in c and c.get("wifi_cap") is not None:
# the first dial of the cycle, whether it was logged before or after the Wi-Fi line
if c.get("dial_before_wifi_cap") is not None:
dw = f"{(c['dial_before_wifi_cap'] - c['wifi_cap']) * 1000:.0f}"
elif "dial_cap" in c and c["dial_cap"] is not None:
dw = f"{(c['dial_cap'] - c['wifi_cap']) * 1000:.0f}"
c["dial-wifi"] = dw
print(
f"{i:>2} {c.get('wifi', ''):>8} {dw:>16} {c.get('client', ''):>10} {cw:>11} {c.get('via', ''):7s} {c.get('dials', 0):>5} {c.get('dial_closed', 0):>6} {c.get('who', '')}"
)
for via in ("dial", "accept"):
vals = [
c["client-wifi"]
for c in cycles
if c.get("via") == via and c["client-wifi"] != ""
]
if vals:
print(
f"client-wifi via {via}: n={len(vals)} min={min(vals)} median={statistics.median(vals):.0f} max={max(vals)}"
)
dws = [float(c["dial-wifi"]) for c in cycles if c.get("dial-wifi") not in ("", None)]
if dws:
print(
f"dial after wifi connected (capture clock): n={len(dws)} min={min(dws):.0f} median={statistics.median(dws):.0f} max={max(dws):.0f}"
)
if loop_max:
print(
f"loop time max per 5 s: n={len(loop_max)} min={min(loop_max)} median={statistics.median(loop_max):.0f} max={max(loop_max)}"
)
@@ -0,0 +1,22 @@
#!/bin/sh
# cases.sh DEV_IP KEY CAPTURE LABEL : the SEN0395 flow (settings, switches, junk lines, factory reset, reset) against
# templates/dfrobot-sen0395-dut.yaml, with a serial capture of the device running into CAPTURE
D=$1; K=$2; LOG=$3; LABEL=$4; PY=${PYTHON:-python3}; CTL="$(dirname "$0")/ctl.py"
show() { tail -n +$(($1+1)) "$LOG" | sed 's/\x1b\[[0-9;]*m//g' | grep -a 'dfrobot\|Detected\|\[W\]\|\[E\]\|sen\]' | grep -a -v 'JYBSS\|took a long time' | cut -c1-200; }
step() { n0=$(wc -l < "$LOG"); echo "=== $LABEL: $1 ($(date +%T))"; shift; $PY "$CTL" "$D" "$K" "$@" || { rc=$?; echo "driver failed ($rc), aborting"; exit $rc; }; echo "--- device log:"; show "$n0"; }
frames() { echo "frames: $(tail -n +$(($1+1)) "$LOG" | grep -a -c 'JYBSS,1') present, $(tail -n +$(($1+1)) "$LOG" | grep -a -c 'JYBSS,0') absent"; }
n0=$(wc -l < "$LOG"); echo "=== $LABEL: 20 s of frames ($(date +%T))"; sleep 20; frames "$n0"
step "range 0 to 3 m" "press:Range 0 to 3m" wait:12
step "range 0 to 9 m" "press:Range 0 to 9m" wait:12
step "latency 1 s / 2 s" "press:Latency 1s 2s" wait:12
step "latency 0 / 0" "press:Latency 0 0" wait:12
step "sensitivity 7" "press:Sensitivity 7" wait:12
step "LED off then on" "sw:LED=off" wait:10 "sw:LED=on" wait:10
step "sensor active off then on" "sw:Sensor active=off" wait:10 "sw:Sensor active=on" wait:10
step "presence via UART off then on" "sw:Presence via UART=off" wait:10 "sw:Presence via UART=on" wait:10
step "start after boot off then on" "sw:Start after boot=off" wait:10 "sw:Start after boot=on" wait:10
for n in 100 160 200 220 240 256; do step "junk line of $n bytes" "press:Junk $n" wait:7; done
step "junk 220 then a command" "press:Junk 220" wait:6 "press:Range 0 to 9m" wait:12
step "factory reset" "press:Factory reset" wait:15
step "reset" "press:Reset" wait:15
n0=$(wc -l < "$LOG"); echo "=== $LABEL: 20 s of frames after ($(date +%T))"; sleep 20; frames "$n0"
@@ -0,0 +1,63 @@
"""Drive the SEN0395 test device over the API and print its binary sensor and switch updates.
usage: ctl.py HOST KEY(- for plaintext) OP [OP ...] OP: press:NAME | sw:NAME=on|off | wait:SECONDS
"""
import asyncio
import sys
import time
from aioesphomeapi import (
APIClient,
BinarySensorState,
ButtonInfo,
SwitchInfo,
SwitchState,
)
HOST, KEY = sys.argv[1], sys.argv[2]
OPS = sys.argv[3:]
T0 = time.time()
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
ents, _ = await c.list_entities_services()
by_key = {e.key: e for e in ents}
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
switches = {e.name: e for e in ents if isinstance(e, SwitchInfo)}
def on_state(s) -> None:
e = by_key.get(s.key)
if isinstance(s, BinarySensorState):
print(
f"{time.time() - T0:7.3f} BIN {e.name} = {'ON' if s.state else 'off'}"
)
elif isinstance(s, SwitchState):
print(
f"{time.time() - T0:7.3f} SW {e.name} = {'on' if s.state else 'off'}"
)
c.subscribe_states(on_state)
await asyncio.sleep(1.0)
for op in OPS:
kind, _, rest = op.partition(":")
if kind == "wait":
await asyncio.sleep(float(rest))
elif kind == "press":
if rest not in buttons:
sys.exit(f"no button {rest!r}; have {sorted(buttons)}")
print(f"{time.time() - T0:7.3f} >>> press {rest}")
c.button_command(buttons[rest].key)
elif kind == "sw":
name, _, val = rest.partition("=")
if name not in switches:
sys.exit(f"no switch {name!r}; have {sorted(switches)}")
print(f"{time.time() - T0:7.3f} >>> switch {name} {val}")
c.switch_command(switches[name].key, val == "on")
else:
sys.exit(f"unknown op {op!r}")
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,78 @@
"""Read or drive a bulb directly over its own ESPHome API (an HA-side change, independent of the mirror).
usage: bulb.py HOST KEY state | on [brightness 0-1] [ct_mireds|rgb r,g,b] | off | restart
"""
import asyncio
import sys
from aioesphomeapi import APIClient, ButtonInfo, LightColorCapability, LightInfo
HOST, KEY, CMD = sys.argv[1], sys.argv[2], sys.argv[3]
ARGS = sys.argv[4:]
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
ents, _ = await c.list_entities_services()
light = next((e for e in ents if isinstance(e, LightInfo)), None)
if light is None:
sys.exit("no light entity on this device; is HOST a bulb?")
if CMD == "state":
fut = asyncio.get_running_loop().create_future()
def on_state(st):
if getattr(st, "key", None) == light.key and not fut.done():
fut.set_result(st)
c.subscribe_states(on_state)
st = await asyncio.wait_for(fut, 5)
print(
f"{light.name}: on={st.state} mode={st.color_mode} bri={st.brightness:.2f} "
f"ct={st.color_temperature:.1f} rgb={st.red:.2f}/{st.green:.2f}/{st.blue:.2f} "
f"white={st.white:.2f} cold={st.cold_white:.2f} warm={st.warm_white:.2f}"
)
elif CMD == "on":
kw = {"state": True}
if ARGS:
kw["brightness"] = float(ARGS[0])
if len(ARGS) > 1:
if "," in ARGS[1]:
r, g, b = (float(x) for x in ARGS[1].split(","))
kw["rgb"] = (r, g, b)
kw["color_mode"] = (
LightColorCapability.RGB
| LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
)
else:
kw["color_temperature"] = float(ARGS[1])
kw["color_mode"] = (
LightColorCapability.COLOR_TEMPERATURE
| LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS
)
c.light_command(light.key, **kw)
print("bulb on", kw)
elif CMD == "off":
c.light_command(light.key, state=False)
print("bulb off")
elif CMD == "restart":
btn = next(
(
e
for e in ents
if isinstance(e, ButtonInfo) and "restart" in e.name.lower()
),
None,
)
if btn is None:
sys.exit("no restart button on this device")
c.button_command(btn.key)
print("bulb restart pressed")
await asyncio.sleep(0.5)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,71 @@
"""Held-report cases for esphome#21144 with persistent API connections, so the steps land inside the 3 s fade.
usage: heldcase.py DEV_IP KEY BULB_IP same|many [BULB_KEY] (KEY or BULB_KEY of - means plaintext)
t0 press "A 50 fade 3s" on the mirror device (3 s fade to 50%)
t0+0.7 set the bulb directly to 90% (Home Assistant reports it to the mirror during the fade: held)
t0+1.4 same: press "A 50 fade 3s" again (same target as the running fade)
many: press "A many calls" (16 calls in one loop pass, the last one 100% with a 1 s fade)
t0+6.5 read the bulb, press Log state so the mirror prints its state
"""
import asyncio
import sys
import time
from aioesphomeapi import APIClient, ButtonInfo, LightInfo, LightState
DEV, KEY, BULB, CASE = sys.argv[1:5]
BULB_KEY = sys.argv[5] if len(sys.argv) > 5 else "-"
if CASE not in ("same", "many"):
sys.exit(f"case must be same or many, got {CASE!r}")
T0 = time.time()
def stamp() -> str:
return f"{time.time() - T0:6.3f}"
async def main() -> None:
dev = APIClient(DEV, 6053, None, noise_psk=None if KEY == "-" else KEY)
bulb = APIClient(BULB, 6053, None, noise_psk=None if BULB_KEY == "-" else BULB_KEY)
await dev.connect(login=True)
await bulb.connect(login=True)
dents, _ = await dev.list_entities_services()
bents, _ = await bulb.list_entities_services()
buttons = {e.name: e for e in dents if isinstance(e, ButtonInfo)}
blight = next(e for e in bents if isinstance(e, LightInfo))
states = {}
def on_bulb(s) -> None:
if isinstance(s, LightState) and s.key == blight.key:
states["bulb"] = s
print(f"{stamp()} bulb state: on={s.state} bri={s.brightness:.2f}")
bulb.subscribe_states(on_bulb)
await asyncio.sleep(1.0)
print(f"{stamp()} >>> bulb to 100% (precondition)")
bulb.light_command(blight.key, state=True, brightness=1.0)
await asyncio.sleep(4.0)
t = time.time()
print(f"{stamp()} >>> t0 press A 50 fade 3s")
dev.button_command(buttons["A 50 fade 3s"].key)
await asyncio.sleep(0.7 - (time.time() - t))
print(f"{stamp()} >>> bulb direct to 90%")
bulb.light_command(blight.key, state=True, brightness=0.9)
await asyncio.sleep(1.4 - (time.time() - t))
name = "A 50 fade 3s" if CASE == "same" else "A many calls"
print(f"{stamp()} >>> press {name}")
dev.button_command(buttons[name].key)
await asyncio.sleep(6.5 - (time.time() - t))
print(f"{stamp()} >>> press Log state (the mirror's state is in the device log)")
dev.button_command(buttons["Log state"].key)
await asyncio.sleep(1.0)
b = states.get("bulb")
print(
f"{stamp()} RESULT bulb "
+ (f"bri={b.brightness:.2f}" if b is not None else "no state")
)
await dev.disconnect()
await bulb.disconnect()
asyncio.run(main())
@@ -0,0 +1,85 @@
"""Drive LD2450 numbers and selects over the API while printing every LD2450 state update.
usage: zonedrive.py HOST KEY(- for plaintext) OP [OP ...]
OP: set:NAME=VALUE | sel:NAME=OPTION | wait:SECONDS | list
Every state update of an entity whose name contains "LD2450" is printed with a timestamp.
"""
import asyncio
import sys
import time
from aioesphomeapi import (
APIClient,
NumberInfo,
NumberState,
SelectInfo,
SelectState,
SensorInfo,
SensorState,
)
HOST, KEY = sys.argv[1], sys.argv[2]
OPS = sys.argv[3:]
T0 = time.time()
def stamp() -> str:
return f"{time.time() - T0:7.3f}"
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
ents, _ = await c.list_entities_services()
by_key = {e.key: e for e in ents}
numbers = {e.name: e for e in ents if isinstance(e, NumberInfo)}
selects = {e.name: e for e in ents if isinstance(e, SelectInfo)}
sensors = {e.name: e for e in ents if isinstance(e, SensorInfo)}
def on_state(s) -> None:
e = by_key.get(s.key)
if e is None or "LD2450" not in e.name:
return
if isinstance(s, NumberState):
print(
f"{stamp()} NUM {e.name} = {s.state:.0f}{' (missing)' if s.missing_state else ''}"
)
elif isinstance(s, SelectState):
print(f"{stamp()} SEL {e.name} = {s.state}")
elif isinstance(s, SensorState) and "Count" in e.name:
print(f"{stamp()} CNT {e.name} = {s.state:.0f}")
c.subscribe_states(on_state)
await asyncio.sleep(1.5)
for op in OPS:
if op == "list":
for n in sorted(numbers):
print("number:", n)
for n in sorted(selects):
print("select:", n, selects[n].options)
for n in sorted(sensors):
if "Count" in n:
print("sensor:", n)
continue
kind, _, rest = op.partition(":")
if kind == "wait":
await asyncio.sleep(float(rest))
continue
name, _, value = rest.partition("=")
if kind == "set":
if name not in numbers:
sys.exit(f"no number named {name!r}; have {sorted(numbers)}")
print(f"{stamp()} >>> set {name} = {value}")
c.number_command(numbers[name].key, float(value))
elif kind == "sel":
if name not in selects:
sys.exit(f"no select named {name!r}; have {sorted(selects)}")
print(f"{stamp()} >>> select {name} = {value}")
c.select_command(selects[name].key, value)
else:
sys.exit(f"unknown op {op!r}")
await asyncio.sleep(0.3)
await c.disconnect()
asyncio.run(main())
@@ -1,6 +1,6 @@
"""Bounded, timestamped serial capture that always exits and closes the port.
serial_capture.py PORT SECONDS OUTFILE [--baud 115200] [--released] [--reset] [--append]
serial_capture.py PORT SECONDS OUTFILE [--baud 115200] [--released] [--reset] [--append] [--ms]
default open with DTR/RTS as pyserial leaves them (asserted); fine for ESP devkits and the Pico.
--released drive DTR and RTS low before opening: the BW15 and BK7238 CH340 kits strap the boot pin
@@ -76,14 +76,21 @@ def open_port(args: argparse.Namespace, path: str) -> serial.Serial:
return s
def write_line(out, tag: bytes, data: bytes) -> None:
stamp = time.strftime("[%H:%M:%S]").encode()
def write_line(out, tag: bytes, data: bytes, ms: bool = False) -> None:
if ms:
now = time.time()
stamp = (
time.strftime("[%H:%M:%S", time.localtime(now))
+ f".{int(now * 1000) % 1000:03d}]"
).encode()
else:
stamp = time.strftime("[%H:%M:%S]").encode()
out.write(stamp + tag + ANSI.sub(b"", SECRET.sub(b"<secret>", data)) + b"\n")
out.flush()
def note(out, text: str) -> None:
write_line(out, b"[capture] ", text.encode())
def note(out, text: str, ms: bool = False) -> None:
write_line(out, b"[capture] ", text.encode(), ms)
def capture(args: argparse.Namespace, out) -> int:
@@ -102,18 +109,18 @@ def capture(args: argparse.Namespace, out) -> int:
if serial_number is None:
serial_number, index = identify(path)
if path != args.port:
note(out, f"reopened as {path}")
note(out, f"reopened as {path}", args.ms)
except PORT_ERRORS as err:
note(out, f"open failed: {err}")
note(out, f"open failed: {err}", args.ms)
time.sleep(1)
continue
try:
data = s.read(4096)
except PORT_ERRORS as err:
if buf: # keep the cut-off line apart from the next boot's first line
write_line(out, b"[partial]", buf)
write_line(out, b"[partial]", buf, args.ms)
buf = b""
note(out, f"port lost: {err}")
note(out, f"port lost: {err}", args.ms)
with contextlib.suppress(*PORT_ERRORS):
s.close()
s = None
@@ -125,9 +132,9 @@ def capture(args: argparse.Namespace, out) -> int:
buf += data
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
write_line(out, b"", line.rstrip(b"\r"))
write_line(out, b"", line.rstrip(b"\r"), args.ms)
if buf:
write_line(out, b"[partial]", buf)
write_line(out, b"[partial]", buf, args.ms)
if s is not None:
s.close()
return received
@@ -144,6 +151,7 @@ def main() -> int:
ap.add_argument("--released", action="store_true")
ap.add_argument("--reset", action="store_true")
ap.add_argument("--append", action="store_true")
ap.add_argument("--ms", action="store_true", help="millisecond timestamps")
args = ap.parse_args()
with pathlib.Path(args.outfile).open("ab" if args.append else "wb") as out:
received = capture(args, out)
@@ -0,0 +1,30 @@
"""Play a URL on the speaker_source media player over the API (no sync task involved).
usage: playurl.py HOST KEY URL [PLAYER_NAME]
"""
import asyncio
import sys
from aioesphomeapi import APIClient, MediaPlayerInfo
HOST, KEY, URL = sys.argv[1], sys.argv[2], sys.argv[3]
PLAYER = sys.argv[4] if len(sys.argv) > 4 else "Player"
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=KEY)
await c.connect(login=True)
ents, _ = await c.list_entities_services()
players = {e.name: e for e in ents if isinstance(e, MediaPlayerInfo)}
print("media players:", sorted(players))
if PLAYER not in players:
sys.exit(f"no media player named {PLAYER!r}; have {sorted(players)}")
p = players[PLAYER]
c.media_player_command(p.key, media_url=URL)
print("sent play_media to", PLAYER)
await asyncio.sleep(1)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,23 @@
# Needs packages/aux-2-pre-prod.yaml from NabuCasa/aux-2 at commit 283f2e64 next to this file, with its local
# external_components source pointed at github://NabuCasa/aux-2@main (see reference/recipes/usb_audio.md).
# Nabu Casa Connect AUX-2 on the usb_audio PR tree: stock package (I2S PCM5122 output) as a control.
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
@@ -0,0 +1,39 @@
# Needs packages/aux-2-pre-prod.yaml from NabuCasa/aux-2 at commit 283f2e64 next to this file, with its local
# external_components source pointed at github://NabuCasa/aux-2@main (see reference/recipes/usb_audio.md).
# Nabu Casa Connect AUX-2 on the usb_audio PR tree: the Anker S330 on the USB host port as the mixer output.
packages:
core: !include aux-2-pre-prod.yaml
esphome:
name: aux-2-usb-dut
name_add_mac_suffix: false
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
encryption:
key: !secret api_key
logger:
level: DEBUG
baud_rate: 0
sendspin:
unpaired_access: true
usb_audio:
id: usb_audio_id
speaker:
- platform: usb_audio
id: usb_speaker
usb_audio_id: usb_audio_id
sample_rate: 48000
bits_per_sample: 24bit
num_channels: 2
buffer_size: 28800
write_timeout: 50ms
- id: !extend mixing_speaker
output_speaker: usb_speaker
@@ -0,0 +1,27 @@
# CAST PRO-1 control for the sendspin sync comparison: Apollo's current package, DEBUG logging as on the S3.
esphome:
name: "cast-pro-1-dut"
friendly_name: CAST PRO-1 test
project:
name: "ApolloAutomation.CAST_PRO-1"
version: ${version}
packages:
core: !include Core.yaml
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
enable_on_boot: false
logger:
level: DEBUG
api:
encryption:
key: !secret api_key
sendspin:
manufacturer: Apollo
model: CAST PRO-1 control
unpaired_access: true
@@ -0,0 +1,46 @@
# Deep sleep wake timing for the outgoing API connection: boot -> dial -> client hello -> first HA state.
esphome:
name: deep-sleep-dut
on_boot:
priority: -100
then:
- lambda: ESP_LOGI("timing", "setup done at %u ms", (unsigned) millis());
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
on_connect:
- lambda: ESP_LOGI("timing", "wifi connected at %u ms", (unsigned) millis());
api:
encryption:
key: !secret api_key
outgoing_connection:
on_client_connected:
- lambda: ESP_LOGI("timing", "client connected at %u ms (%s %s)", (unsigned) millis(), client_info.c_str(), client_address.c_str());
on_client_disconnected:
- lambda: ESP_LOGI("timing", "client disconnected at %u ms", (unsigned) millis());
ota:
- platform: esphome
text_sensor:
- platform: homeassistant
id: ha_state
entity_id: light.ENTITY
on_value:
- lambda: ESP_LOGI("timing", "first HA state '%s' at %u ms", x.c_str(), (unsigned) millis());
deep_sleep:
id: ds
run_duration: 15s
sleep_duration: 15s
@@ -0,0 +1,186 @@
# DFRobot SEN0395 on an ESP32 DevKit V1: V to VIN (5 V), G to GND, sensor TX to GPIO16 (RX2), sensor RX to GPIO17 (TX2),
# IO2 to GPIO18, I1 unconnected. Settings buttons, the four switches and junk-line buttons whose echo and
# "is not recognized" reply come back as lines of known length (220 and 240 bytes give replies over 252 chars).
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: VERBOSE
logs:
api: DEBUG
api.connection: DEBUG
api.service: DEBUG
wifi: DEBUG
mdns: DEBUG
scheduler: DEBUG
component: DEBUG
esp32: DEBUG
uart: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
ota:
- platform: esphome
uart:
id: mmwave_uart
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 115200
dfrobot_sen0395:
id: mmwave
uart_id: mmwave_uart
binary_sensor:
- platform: dfrobot_sen0395
dfrobot_sen0395_id: mmwave
name: Detected via UART
id: uart_detected
- platform: gpio
name: Detected via GPIO
device_class: motion
pin:
number: GPIO18
mode: INPUT_PULLDOWN
switch:
- platform: dfrobot_sen0395
dfrobot_sen0395_id: mmwave
type: sensor_active
name: Sensor active
- platform: dfrobot_sen0395
dfrobot_sen0395_id: mmwave
type: presence_via_uart
name: Presence via UART
- platform: dfrobot_sen0395
dfrobot_sen0395_id: mmwave
type: turn_on_led
name: LED
- platform: dfrobot_sen0395
dfrobot_sen0395_id: mmwave
type: start_after_boot
name: Start after boot
button:
- platform: template
name: Range 0 to 3m
on_press:
- dfrobot_sen0395.settings:
id: mmwave
detection_segments:
- [0cm, 3m]
- platform: template
name: Range 0 to 9m
on_press:
- dfrobot_sen0395.settings:
id: mmwave
detection_segments:
- [0cm, 9m]
- platform: template
name: Latency 1s 2s
on_press:
- dfrobot_sen0395.settings:
id: mmwave
output_latency:
delay_after_detect: 1s
delay_after_disappear: 2s
- platform: template
name: Latency 0 0
on_press:
- dfrobot_sen0395.settings:
id: mmwave
output_latency:
delay_after_detect: 0s
delay_after_disappear: 0s
- platform: template
name: Sensitivity 7
on_press:
- dfrobot_sen0395.settings:
id: mmwave
sensitivity: 7
- platform: template
name: Factory reset
on_press:
- dfrobot_sen0395.settings:
id: mmwave
factory_reset: true
- platform: template
name: Reset
on_press:
- dfrobot_sen0395.reset:
id: mmwave
- platform: template
name: Junk 100
on_press:
- uart.write:
id: mmwave_uart
data: !lambda |-
std::vector<uint8_t> v(100, 'x');
v.push_back('\r');
v.push_back('\n');
return v;
- platform: template
name: Junk 160
on_press:
- uart.write:
id: mmwave_uart
data: !lambda |-
std::vector<uint8_t> v(160, 'x');
v.push_back('\r');
v.push_back('\n');
return v;
- platform: template
name: Junk 200
on_press:
- uart.write:
id: mmwave_uart
data: !lambda |-
std::vector<uint8_t> v(200, 'x');
v.push_back('\r');
v.push_back('\n');
return v;
- platform: template
name: Junk 220
on_press:
- uart.write:
id: mmwave_uart
data: !lambda |-
std::vector<uint8_t> v(220, 'x');
v.push_back('\r');
v.push_back('\n');
return v;
- platform: template
name: Junk 240
on_press:
- uart.write:
id: mmwave_uart
data: !lambda |-
std::vector<uint8_t> v(240, 'x');
v.push_back('\r');
v.push_back('\n');
return v;
- platform: template
name: Junk 256
on_press:
- uart.write:
id: mmwave_uart
data: !lambda |-
std::vector<uint8_t> v(256, 'x');
v.push_back('\r');
v.push_back('\n');
return v;
interval:
- interval: 30s
then:
- lambda: 'ESP_LOGI("sen", "alive uart_detected=%d heap=%u", id(uart_detected).state, (unsigned) esp_get_free_heap_size());'
@@ -0,0 +1,66 @@
# Restoring globals with every storage choice; each boot bumps the counters, logs them with the reset reason, saves
# and syncs; odd boots soft restart, even boots deep sleep (ESP32) or idle (D1 mini). The host's reset pulse and a
# power cut give the other reset kinds.
logger:
level: DEBUG
preferences:
flash_write_interval: 1s
globals:
- id: cnt_rtc
type: int
restore_value: true
storage: rtc
initial_value: "0"
- id: cnt_flash
type: int
restore_value: true
storage: flash
initial_value: "0"
- id: cnt_default
type: int
restore_value: true
initial_value: "0"
- id: str_rtc
type: std::string
restore_value: true
storage: rtc
max_restore_data_length: 24
initial_value: '"fresh"'
- id: arr_rtc
type: uint8_t[2]
restore_value: true
storage: rtc
initial_value: "{0, 0}"
esphome:
on_boot:
priority: -100
then:
- lambda: |-
id(cnt_rtc) += 1;
id(cnt_flash) += 1;
id(cnt_default) += 1;
id(arr_rtc)[0] += 1;
id(arr_rtc)[1] += 2;
char buf[24];
snprintf(buf, sizeof(buf), "boot%d", id(cnt_rtc));
ESP_LOGI("gl", "restored rtc=%d flash=%d default=%d str='%s' arr=%u/%u (values before this boot's bump: one less)",
id(cnt_rtc) - 1, id(cnt_flash) - 1, id(cnt_default) - 1, id(str_rtc).c_str(), id(arr_rtc)[0] - 1, id(arr_rtc)[1] - 2);
id(str_rtc) = buf;
- delay: 2s
- lambda: |-
global_preferences->sync();
ESP_LOGI("gl", "saved rtc=%d flash=%d default=%d str='%s' arr=%u/%u", id(cnt_rtc), id(cnt_flash), id(cnt_default),
id(str_rtc).c_str(), id(arr_rtc)[0], id(arr_rtc)[1]);
- if:
condition:
lambda: 'return id(cnt_rtc) % 2 == 1;'
then:
- delay: 3s
- lambda: 'ESP_LOGI("gl", "odd boot, soft restart now"); App.safe_reboot();'
button:
- platform: restart
name: Restart
@@ -0,0 +1,159 @@
# shared part: two Home Assistant lights mirrored on the device plus buttons that drive them
esphome:
# A local change made before the light core's first write (b19a776 sends it instead of merging it).
on_boot:
priority: -100
then:
- lambda: 'ESP_LOGI("halight", "on_boot turning B on at 35 percent before the first write");'
- light.turn_on:
id: bulb_b
brightness: 35%
logger:
level: VERBOSE
logs:
api: DEBUG
api.connection: DEBUG
api.service: DEBUG
wifi: DEBUG
scheduler: DEBUG
component: DEBUG
sensor: DEBUG
mdns: DEBUG
ota: DEBUG
socket: DEBUG
esp-idf: DEBUG
preferences: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
ota:
- platform: esphome
light:
- platform: homeassistant
id: bulb_a
name: Mirror A
entity_id: light.ENTITY_A
report_timeout: 5s
on_turn_on:
- logger.log: "bulb_a on_turn_on"
on_turn_off:
- logger.log: "bulb_a on_turn_off"
- platform: homeassistant
id: bulb_b
name: Mirror B
entity_id: light.ENTITY_B
report_timeout: 5s
button:
- platform: template
name: A warm 40
id: a_warm_40
on_press:
- light.turn_on:
id: bulb_a
brightness: 40%
color_temperature: 370 mireds
- platform: template
name: A cool 100
on_press:
- light.turn_on:
id: bulb_a
brightness: 100%
color_temperature: 153 mireds
- platform: template
name: A red
on_press:
- light.turn_on:
id: bulb_a
brightness: 80%
red: 100%
green: 0%
blue: 0%
- platform: template
name: A green dim
on_press:
- light.turn_on:
id: bulb_a
brightness: 20%
red: 0%
green: 100%
blue: 0%
- platform: template
name: A brightness only 60
on_press:
- light.turn_on:
id: bulb_a
brightness: 60%
- platform: template
name: A transition 100
on_press:
- light.turn_on:
id: bulb_a
brightness: 100%
transition_length: 3s
- platform: template
name: A flash
on_press:
- light.turn_on:
id: bulb_a
flash_length: 1s
- platform: template
name: A off
on_press:
- light.turn_off: bulb_a
- platform: template
name: A toggle
on_press:
- light.toggle: bulb_a
- platform: template
name: B warm 50
on_press:
- light.turn_on:
id: bulb_b
brightness: 50%
color_temperature: 300 mireds
- platform: template
name: B off
on_press:
- light.turn_off: bulb_b
- platform: template
name: A 50 fade 3s
on_press:
- light.turn_on:
id: bulb_a
brightness: 50%
transition_length: 3s
- platform: template
name: A many calls
on_press:
- lambda: |-
for (int i = 0; i < 16; i++) {
id(bulb_a).make_call().set_brightness(i == 15 ? 1.0f : 0.6f).set_transition_length(1000).perform();
}
- platform: template
name: Log state
on_press:
- lambda: |-
auto v = id(bulb_a).current_values;
ESP_LOGI("halight", "A: on=%d mode=%d bri=%.2f ct=%.1f rgb=%.2f/%.2f/%.2f B: on=%d bri=%.2f",
v.is_on(), (int) v.get_color_mode(), v.get_brightness(), v.get_color_temperature(),
v.get_red(), v.get_green(), v.get_blue(), id(bulb_b).current_values.is_on(),
id(bulb_b).current_values.get_brightness());
interval:
- interval: 30s
then:
- lambda: |-
auto v = id(bulb_a).current_values;
ESP_LOGI("halight", "alive A: on=%d mode=%d bri=%.2f ct=%.1f rgb=%.2f/%.2f/%.2f B: on=%d bri=%.2f",
v.is_on(), (int) v.get_color_mode(), v.get_brightness(), v.get_color_temperature(),
v.get_red(), v.get_green(), v.get_blue(), id(bulb_b).current_values.is_on(),
id(bulb_b).current_values.get_brightness());
@@ -0,0 +1,55 @@
# Mains build (no deep sleep): outgoing API connection timing after a Wi-Fi drop, and the loop cost while a
# connect is pending or the network is down.
esphome:
name: wifi-drop-dut
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
on_connect:
- lambda: ESP_LOGI("timing", "wifi connected at %u ms", (unsigned) millis());
on_disconnect:
- lambda: ESP_LOGI("timing", "wifi disconnected at %u ms", (unsigned) millis());
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
outgoing_connection:
delay: 2s
on_client_connected:
- lambda: ESP_LOGI("timing", "client connected at %u ms (%s %s)", (unsigned) millis(), client_info.c_str(), client_address.c_str());
on_client_disconnected:
- lambda: ESP_LOGI("timing", "client disconnected at %u ms (%s)", (unsigned) millis(), client_info.c_str());
ota:
- platform: esphome
debug:
update_interval: 5s
sensor:
- platform: debug
loop_time:
name: Loop Time
on_value:
- lambda: ESP_LOGI("timing", "loop time max %.0f ms over the last 5 s at %u ms", x, (unsigned) millis());
button:
- platform: template
name: Wifi cycle
on_press:
- lambda: ESP_LOGI("timing", "wifi cycle disabling at %u ms", (unsigned) millis());
- wifi.disable:
- delay: 4s
- lambda: ESP_LOGI("timing", "wifi cycle enabling at %u ms", (unsigned) millis());
- wifi.enable:
@@ -0,0 +1,76 @@
# Music Assistant target: the plain USB audio template plus the CAST-1 style sendspin wiring (resampler speakers
# into a mixer whose inputs never time out, speaker_source media player, sendspin hub and sources).
packages:
base: !include usb-audio-waveshare-s3-eth-dut.yaml
# Logs over the API only: a DEBUG line per report would saturate UART0 and trip the task watchdog.
logger:
baud_rate: 0
speaker:
- id: !extend usb_speaker
buffer_size: 28800
write_timeout: 50ms
- platform: mixer
id: mixer_speaker_id
output_speaker: usb_speaker
num_channels: 2
task_stack_in_psram: true
source_speakers:
- id: announcement_mixer_input
timeout: never
- id: media_mixer_input
timeout: never
- platform: resampler
id: announcement_resampling_speaker
output_speaker: announcement_mixer_input
sample_rate: 48000
bits_per_sample: 16
- platform: resampler
id: media_resampling_speaker
output_speaker: media_mixer_input
sample_rate: 48000
bits_per_sample: 16
sendspin:
id: sendspin_hub
# PSRAM stack skews time sync; keep internal
task_stack_in_psram: false
manufacturer: Waveshare
model: ESP32-S3-ETH USB audio
firmware_version: "0.1"
unpaired_access: true
media_source:
- platform: sendspin
id: sendspin_source
- platform: audio_http
id: http_media_source
buffer_size: 500000
- platform: audio_http
id: http_announcement_source
buffer_size: 250000
media_player:
- platform: sendspin
id: sendspin_group_media_player
name: Sendspin Player
- platform: speaker_source
id: external_media_player
name: Player
volume_increment: 0.05
announcement_pipeline:
format: FLAC
num_channels: 1
sample_rate: 48000
speaker: announcement_resampling_speaker
sources:
- http_announcement_source
media_pipeline:
speaker: media_resampling_speaker
format: FLAC
num_channels: 2
sample_rate: 48000
sources:
- sendspin_source
- http_media_source
@@ -0,0 +1,142 @@
# Waveshare ESP32-S3-ETH: USB host for a speakerphone (usb_audio), logs over Ethernet, the USB port is the host port
esphome:
name: usb-audio-dut
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
psram:
mode: octal
speed: 80MHz
logger:
level: DEBUG
hardware_uart: UART0
logs:
usb_host: DEBUG
ethernet:
type: W5500
clk_pin: GPIO13
mosi_pin: GPIO11
miso_pin: GPIO12
cs_pin: GPIO14
interrupt_pin: GPIO10
reset_pin: GPIO9
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
ota:
- platform: esphome
usb_audio:
id: usb_audio_id
microphone:
- platform: usb_audio
id: usb_mic
usb_audio_id: usb_audio_id
sample_rate: 48000
bits_per_sample: 16bit
num_channels: 2
buffer_size: 9600
on_data:
- lambda: |-
id(mic_bytes) += x.size();
id(mic_chunks) += 1;
speaker:
- platform: usb_audio
id: usb_speaker
usb_audio_id: usb_audio_id
sample_rate: 48000
bits_per_sample: 24bit
num_channels: 2
buffer_size: 9600
write_timeout: 20ms
globals:
- id: beep_left
type: int
initial_value: "0"
- id: beep_written
type: uint32_t
initial_value: "0"
- id: beep_short
type: uint32_t
initial_value: "0"
- id: beep_phase
type: float
initial_value: "0"
- id: mic_bytes
type: uint32_t
initial_value: "0"
- id: mic_chunks
type: uint32_t
initial_value: "0"
button:
- platform: template
name: Mic start
on_press:
- microphone.capture: usb_mic
- platform: template
name: Mic stop
on_press:
- microphone.stop_capture: usb_mic
- platform: template
name: Beep
on_press:
- lambda: |-
// 2 s of a 1 kHz tone at 48 kHz 24-bit stereo, fed 10 ms at a time from the interval below
id(beep_left) = 200;
id(beep_written) = 0;
id(beep_short) = 0;
id(beep_phase) = 0.0f;
id(usb_speaker).start();
ESP_LOGI("uatest", "beep started");
- platform: template
name: Speaker stop
on_press:
- speaker.stop: usb_speaker
- platform: template
name: Restart
on_press:
- lambda: App.safe_reboot();
interval:
- interval: 8ms
then:
- lambda: |-
if (id(beep_left) <= 0) return;
// 10 ms of 48 kHz stereo 24-bit samples, 3 bytes little-endian per sample
static uint8_t buf[480 * 2 * 3];
float phase = id(beep_phase);
for (int i = 0; i < 480; i++) {
int32_t s = (int32_t) (1500000.0f * sinf(phase));
phase += 2.0f * 3.14159265f * 1000.0f / 48000.0f;
if (phase > 6.2831853f) phase -= 6.2831853f;
for (int c = 0; c < 2; c++) {
uint8_t *o = buf + (i * 2 + c) * 3;
o[0] = s & 0xFF; o[1] = (s >> 8) & 0xFF; o[2] = (s >> 16) & 0xFF;
}
}
size_t n = id(usb_speaker).play(buf, sizeof(buf), 0);
if (n == 0) { id(beep_short) += 1; return; } // not accepted yet, try the same chunk next time
id(beep_phase) = phase;
id(beep_written) += n;
id(beep_left) -= 1;
if (id(beep_left) == 0) ESP_LOGI("uatest", "beep done: %u bytes queued, %u retries", (unsigned) id(beep_written), (unsigned) id(beep_short));
- interval: 5s
then:
- lambda: |-
ESP_LOGI("uatest", "mic capturing=%d bytes=%u chunks=%u | speaker running=%d | heap free %u",
id(usb_mic).is_running(), (unsigned) id(mic_bytes), (unsigned) id(mic_chunks),
id(usb_speaker).is_running(), (unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
+2 -1
View File
@@ -313,6 +313,7 @@ jobs:
- "3.12"
- "3.13"
- "3.14"
- "3.15"
os:
- ubuntu-latest
- macOS-latest
@@ -629,7 +630,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
uses: CodSpeedHQ/action@821a40cd6c80e3cfe387caf55668287addd808da # v5.5.0
with:
run: |
. venv/bin/activate
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
permissions:
issues: write # issues.lock on closed issues
pull-requests: write # issues.lock on closed pull requests
uses: esphome/workflows/.github/workflows/lock.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # 2026.9.0
uses: esphome/workflows/.github/workflows/lock.yml@31d160e5f0ffa0ee65d5b27cb55c153d415f2abe # 2026.10.2
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@4fbce13026c393d6eb6b247e394dcd9ef1af76c2 # main
uses: esphome/workflows/.github/workflows/stale.yml@31d160e5f0ffa0ee65d5b27cb55c153d415f2abe # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
+1 -1
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.17.0
rev: v0.17.1
hooks:
# Run the linter.
- id: ruff
+28 -4
View File
@@ -80,13 +80,15 @@ file does, and it is the authority when they disagree. The most useful starting
```python
# Bad - looks up CONF_BLAH twice
if CONF_BLAH in config:
cg.add(var.set_blah(config[CONF_BLAH]))
cg.add(var.set_blah_ms(config[CONF_BLAH].total_milliseconds))
# Good - single lookup, value bound inline
if (blah := config.get(CONF_BLAH)) is not None:
cg.add(var.set_blah(blah))
cg.add(var.set_blah_ms(blah.total_milliseconds))
```
The same applies to `while` loops and comprehensions where it avoids recomputing a value. Don't contort code to use it — reach for `:=` only when it genuinely cuts repetition or an extra assignment line.
When the setter takes the value unchanged, use `cg.add_optional(var.set_blah, config.get(CONF_BLAH))`
instead; it is not awaited, and it skips only `None`, so `0` and `False` are still set.
* **C++ Field Visibility:**
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
@@ -208,8 +210,14 @@ file does, and it is the authority when they disagree. The most useful starting
* **Fixed sets of names** (status words, model names): a `PROGMEM_STRING_TABLE` (`esphome/core/progmem.h`),
logged with `get_log_str` and `LOG_STR_ARG`, copied out with `ESPHOME_strncpy_P` before `publish_state`.
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `reverse_bits`, `encode_uint16`);
pick the helper's polynomial and bit-order arguments instead of writing a CRC loop.
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `xor_bytes`, `sum16_bytes`,
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`); pick the helper's
arguments instead of writing a loop. Epson style 7-byte RTC calendars (one-hot weekday) use
`time::decode_bcd_calendar`.
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
end of `setup()` when the entities need a state at boot.
@@ -429,6 +437,22 @@ file does, and it is the authority when they disagree. The most useful starting
and `await cg.add_variable(var.set_bus, config[CONF_BUS_ID])`; both skip a `None` value, so pass
`config.get(KEY)` for an optional key, and return what they created. Use `cg.register_parented` for `set_parent`.
- **Hub forwarding entities need no C++ class:** a button, switch, number or select that only calls its hub
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state.
```python
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
```
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
constructs, registers and attaches in one call; `display.add_writer(var, config)` sets the lambda;
`MyEnum.options("on", prefix="MY_ENUM_")` builds an enum dict; `button.restart_button_schema(cls)` (and
`factory_reset_`, `query_`) are button presets; `fv.get_declaring_config(id)` finds a hub's config in
final validation.
* **Automations (Triggers, Actions, Conditions):**
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
+5 -1
View File
@@ -133,7 +133,7 @@ esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/cm1106/* @andrewjswan
esphome/components/color_temperature/* @jesserockz
esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/combination/* @kahrendt
esphome/components/const/* @esphome/core
esphome/components/coolix/* @glmnet
esphome/components/copy/* @OttoWinter
@@ -601,7 +601,10 @@ esphome/components/ssd1331_base/* @kbx81
esphome/components/ssd1331_spi/* @kbx81
esphome/components/ssd1351_base/* @kbx81
esphome/components/ssd1351_spi/* @kbx81
esphome/components/st25r/* @JohnMcLear
esphome/components/st25r_spi/* @JohnMcLear
esphome/components/st7123/* @miniskipper
esphome/components/st7305/* @clydebarrow
esphome/components/st7567_base/* @latonita
esphome/components/st7567_i2c/* @latonita
esphome/components/st7567_spi/* @latonita
@@ -684,6 +687,7 @@ esphome/components/ultrasonic/* @ssieb @swoboda1337
esphome/components/unicode/* @esphome/core
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_audio/* @ab-tools
esphome/components/usb_cdc_acm/* @kbx81
esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow
+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.24.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.25.0
RUN \
platformio settings set enable_telemetry No \
+1
View File
@@ -791,6 +791,7 @@ def write_cpp(config: ConfigType) -> int:
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
writer.check_build_tree_not_tracked()
generate_cpp_contents(config)
return write_cpp_file()
+11 -3
View File
@@ -430,12 +430,20 @@ def _apply_function(
) -> MockObj:
"""Emit the generated function and declare ``id_`` as the ``ApplyAction`` or
``ApplyCondition`` templated on it, so ``play()`` calls it directly."""
fn = cg.static_function(
f"esphome__{id_.id}__fn", return_type, lambda_args, statements
)
fn = apply_static_function(id_, return_type, lambda_args, statements)
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
def apply_static_function(
id_: ID,
return_type: SafeExpType,
params: TemplateArgsType,
statements: list[str],
) -> MockObj:
"""Emit the static function generated for ``id_`` and return an expression naming it."""
return cg.static_function(f"esphome__{id_.id}__fn", return_type, params, statements)
async def _render_values(
name: str,
target: str,
+1
View File
@@ -53,6 +53,7 @@ from esphome.cpp_generator import ( # noqa: F401
with_local_variable,
)
from esphome.cpp_helpers import ( # noqa: F401
add_optional,
add_pin,
add_variable,
build_registry_entry,
+5 -17
View File
@@ -136,13 +136,7 @@ void AirtonClimate::transmit_state() {
data->set_carrier_frequency(AIRTON_IR_FREQUENCY);
data->mark(AIRTON_HEADER_MARK);
data->space(AIRTON_HEADER_SPACE);
for (uint8_t payload_byte : remote_state) {
for (uint8_t payload_bit_cursor = 0; payload_bit_cursor < 8; payload_bit_cursor++) {
data->mark(AIRTON_BIT_MARK);
bool bit = payload_byte & (1 << payload_bit_cursor);
data->space(bit ? AIRTON_ONE_SPACE : AIRTON_ZERO_SPACE);
}
}
data->item_bytes<AIRTON_BIT_TIMINGS>(remote_state, AIRTON_STATE_FRAME_SIZE);
data->mark(AIRTON_BIT_MARK);
data->space(AIRTON_MESSAGE_SPACE);
@@ -284,20 +278,14 @@ void AirtonClimate::parse_state_frame_(const uint8_t *frame) {
}
bool AirtonClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t remote_state[AIRTON_STATE_FRAME_SIZE] = {};
uint8_t remote_state[AIRTON_STATE_FRAME_SIZE];
if (!data.expect_item(AIRTON_HEADER_MARK, AIRTON_HEADER_SPACE)) {
ESP_LOGV(TAG, "Wrong header encoding detected!");
return false;
}
for (int i = 0; i < AIRTON_STATE_FRAME_SIZE; i++) {
for (int j = 0; j < 8; j++) {
if (data.expect_item(AIRTON_BIT_MARK, AIRTON_ONE_SPACE)) {
remote_state[i] |= 1 << j;
} else if (!data.expect_item(AIRTON_BIT_MARK, AIRTON_ZERO_SPACE)) {
ESP_LOGV(TAG, "Wrong modulation encoding for: Byte %d, bit %d", i, j);
return false;
}
}
if (!data.expect_bytes<AIRTON_BIT_TIMINGS>(remote_state, AIRTON_STATE_FRAME_SIZE)) {
ESP_LOGV(TAG, "Wrong modulation encoding");
return false;
}
if (remote_state[0] != AIRTON_HEADER_BYTE0 || remote_state[1] != AIRTON_HEADER_BYTE1) {
+2 -2
View File
@@ -36,8 +36,8 @@ static constexpr uint32_t AIRTON_IR_FREQUENCY = 38000;
static constexpr uint32_t AIRTON_HEADER_MARK = 6630;
static constexpr uint32_t AIRTON_HEADER_SPACE = 3350;
static constexpr uint32_t AIRTON_BIT_MARK = 400;
static constexpr uint32_t AIRTON_ONE_SPACE = 1260;
static constexpr uint32_t AIRTON_ZERO_SPACE = 430;
static constexpr remote_base::BitTimings AIRTON_BIT_TIMINGS{
.bit_mark = AIRTON_BIT_MARK, .one_space = 1260, .zero_space = 430};
static constexpr uint32_t AIRTON_MESSAGE_SPACE = 100000;
static constexpr uint8_t AIRTON_STATE_FRAME_SIZE = 7;
+6 -5
View File
@@ -8,19 +8,18 @@ from ..climate import CONF_AIRTON_ID, CONF_SLEEP_MODE, AirtonClimate, airton_ns
CODEOWNERS = ["@Procsiab"]
AirtonSwitch = airton_ns.class_("AirtonSwitch", switch.Switch)
AirtonSetting = airton_ns.enum("AirtonSetting", is_class=True)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_AIRTON_ID): cv.use_id(AirtonClimate),
cv.Optional(CONF_DISPLAY): switch.switch_schema(
AirtonSwitch,
switch.ApplySwitch,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
cv.Optional(CONF_SLEEP_MODE): switch.switch_schema(
AirtonSwitch,
switch.ApplySwitch,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
@@ -32,10 +31,12 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_AIRTON_ID])
switches = switch.sub_switches(config, parent=hub)
await switches(
CONF_DISPLAY, hub.set_display_switch, AirtonSetting.AIRTON_SETTING_DISPLAY
CONF_DISPLAY,
hub.set_display_switch,
apply=f"set_setting({AirtonSetting.AIRTON_SETTING_DISPLAY}, state)",
)
await switches(
CONF_SLEEP_MODE,
hub.set_sleep_mode_switch,
AirtonSetting.AIRTON_SETTING_SLEEP_MODE,
apply=f"set_setting({AirtonSetting.AIRTON_SETTING_SLEEP_MODE}, state)",
)
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../airton.h"
namespace esphome::airton {
// One of the unit's on/off settings; the parent sends the state and publishes it back
class AirtonSwitch : public switch_::Switch, public Parented<AirtonClimate> {
public:
explicit AirtonSwitch(AirtonSetting setting) : setting_(setting) {}
protected:
void write_state(bool state) override { this->parent_->set_setting(this->setting_, state); }
AirtonSetting setting_;
};
} // namespace esphome::airton
+6 -6
View File
@@ -142,7 +142,7 @@ message HelloResponse {
// A string identifying the server (ESP); like client info this may be empty
// and only exists for debugging/logging purposes.
// Currently set to ESPHOME_VERSION string literal.
string server_info = 3 [(max_data_length) = 32, (force) = true];
string server_info = 3 [(max_data_length) = 32, (force) = true, (progmem) = true];
// The name of the server (App.get_name() - device hostname)
// max_data_length matches ESPHOME_DEVICE_NAME_MAX_LEN (validated by validate_hostname)
@@ -278,7 +278,7 @@ message DeviceInfoResponse {
string mac_address = 3 [(max_data_length) = 17, (force) = true];
// A string describing the ESPHome version. For example "1.10.0"
string esphome_version = 4 [(max_data_length) = 32, (force) = true];
string esphome_version = 4 [(max_data_length) = 32, (force) = true, (progmem) = true];
// A string describing the date of compilation, this is generated by the compiler
// and therefore may not be in the same format all the time.
@@ -287,14 +287,14 @@ message DeviceInfoResponse {
// The model of the board. For example NodeMCU
// max_data_length matches core/config.BOARD_MAX_LENGTH (validated in platform schemas)
string model = 6 [(max_data_length) = 127, (force) = true];
string model = 6 [(max_data_length) = 127, (force) = true, (progmem) = true];
bool has_deep_sleep = 7 [(field_ifdef) = "USE_DEEP_SLEEP"];
// The esphome project details if set
// max_data_length matches core/config.PROJECT_MAX_LENGTH
string project_name = 8 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"];
string project_version = 9 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"];
string project_name = 8 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME", (progmem) = true];
string project_version = 9 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME", (progmem) = true];
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
@@ -304,7 +304,7 @@ message DeviceInfoResponse {
// Superseded by DeviceCapabilitiesResponse.bluetooth_proxy as of API 1.15.
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
string manufacturer = 12 [(max_data_length) = 20, (force) = true];
string manufacturer = 12 [(max_data_length) = 20, (force) = true, (progmem) = true];
string friendly_name = 13 [(max_data_length) = 120, (force) = true];
+13 -39
View File
@@ -28,6 +28,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/version.h"
#ifdef USE_PROVISIONING
#include "esphome/components/provisioning/provisioning.h"
@@ -84,8 +85,6 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS *
// 28-30s. See https://github.com/esphome/esphome/issues/14999
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
// Cross-validate C++ constants against proto max_data_length annotations in api.proto
static_assert(MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1 == 17,
"Update max_data_length for mac_address/bluetooth_mac_address in api.proto");
@@ -1899,6 +1898,11 @@ void APIConnection::complete_authentication_() {
#endif
}
// A flash string on ESP8266, a mergeable literal elsewhere; #undef after the device info reply
#define PROGMEM_STRING_REF(s) ProgmemStringRef(ESPHOME_PSTR(s), sizeof(s) - 1)
// One flash copy for the hello and device info replies
static ProgmemStringRef esphome_version_ref() { return PROGMEM_STRING_REF(ESPHOME_VERSION); }
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
// Copy client name with truncation if needed (set_client_name handles truncation)
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
@@ -1914,7 +1918,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
resp.api_version_major = 1;
resp.api_version_minor = 19;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = ESPHOME_VERSION_REF;
resp.server_info = esphome_version_ref();
resp.name = StringRef(App.get_name());
#ifdef USE_PROVISIONING
@@ -1969,14 +1973,13 @@ bool APIConnection::send_device_info_response_() {
format_mac_addr_upper(mac, mac_address);
resp.mac_address = StringRef(mac_address);
resp.esphome_version = ESPHOME_VERSION_REF;
resp.esphome_version = esphome_version_ref();
// Stack buffer for build time string
char build_time_str[Application::BUILD_TIME_STR_SIZE];
App.get_build_time_string(build_time_str);
resp.compilation_time = StringRef(build_time_str);
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
#if defined(USE_ESP8266) || defined(USE_ESP32)
#define ESPHOME_MANUFACTURER "Espressif"
#elif defined(USE_RP2)
@@ -1993,48 +1996,19 @@ bool APIConnection::send_device_info_response_() {
#define ESPHOME_MANUFACTURER "Host"
#endif
#ifdef USE_ESP8266
// ESP8266 requires PROGMEM for flash storage, copy to stack for memcpy compatibility
static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER;
char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)];
memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM));
resp.manufacturer = StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1);
#else
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
resp.manufacturer = MANUFACTURER;
#endif
resp.manufacturer = PROGMEM_STRING_REF(ESPHOME_MANUFACTURER);
static_assert(sizeof(ESPHOME_MANUFACTURER) - 1 <= 20, "Update max_data_length for manufacturer in api.proto");
#undef ESPHOME_MANUFACTURER
#ifdef USE_ESP8266
static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD;
char model_buf[sizeof(MODEL_PROGMEM)];
memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM));
resp.model = StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1);
#else
static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
resp.model = MODEL;
#endif
resp.model = PROGMEM_STRING_REF(ESPHOME_BOARD);
#ifdef USE_DEEP_SLEEP
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
#endif
#ifdef ESPHOME_PROJECT_NAME
#ifdef USE_ESP8266
static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME;
static const char PROJECT_VERSION_PROGMEM[] PROGMEM = ESPHOME_PROJECT_VERSION;
char project_name_buf[sizeof(PROJECT_NAME_PROGMEM)];
char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)];
memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM));
memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM));
resp.project_name = StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1);
resp.project_version = StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1);
#else
static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
resp.project_name = PROJECT_NAME;
resp.project_version = PROJECT_VERSION;
#endif
resp.project_name = PROGMEM_STRING_REF(ESPHOME_PROJECT_NAME);
resp.project_version = PROGMEM_STRING_REF(ESPHOME_PROJECT_VERSION);
#endif
#undef PROGMEM_STRING_REF
#ifdef USE_WEBSERVER
resp.webserver_port = USE_WEBSERVER_PORT;
#endif
+6 -6
View File
@@ -31,7 +31,7 @@ uint8_t *HelloResponse::encode_msg(const void *self, uint8_t *__restrict__ pos P
const auto &msg = *static_cast<const HelloResponse *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.api_version_major);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.api_version_minor);
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, msg.server_info);
pos = ProtoEncode::encode_short_progmem_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, msg.server_info);
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 34, msg.name);
return pos;
}
@@ -117,17 +117,17 @@ uint8_t *DeviceInfoResponse::encode_msg(const void *self, uint8_t *__restrict__
const auto &msg = *static_cast<const DeviceInfoResponse *>(self);
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 18, msg.name);
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, msg.mac_address);
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 34, msg.esphome_version);
pos = ProtoEncode::encode_short_progmem_string_force(pos PROTO_ENCODE_DEBUG_ARG, 34, msg.esphome_version);
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 42, msg.compilation_time);
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 50, msg.model);
pos = ProtoEncode::encode_short_progmem_string_force(pos PROTO_ENCODE_DEBUG_ARG, 50, msg.model);
#ifdef USE_DEEP_SLEEP
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.has_deep_sleep);
#endif
#ifdef ESPHOME_PROJECT_NAME
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 66, msg.project_name);
pos = ProtoEncode::encode_short_progmem_string_force(pos PROTO_ENCODE_DEBUG_ARG, 66, msg.project_name);
#endif
#ifdef ESPHOME_PROJECT_NAME
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 74, msg.project_version);
pos = ProtoEncode::encode_short_progmem_string_force(pos PROTO_ENCODE_DEBUG_ARG, 74, msg.project_version);
#endif
#ifdef USE_WEBSERVER
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, msg.webserver_port);
@@ -135,7 +135,7 @@ uint8_t *DeviceInfoResponse::encode_msg(const void *self, uint8_t *__restrict__
#ifdef USE_BLUETOOTH_PROXY
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 15, msg.bluetooth_proxy_feature_flags);
#endif
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 98, msg.manufacturer);
pos = ProtoEncode::encode_short_progmem_string_force(pos PROTO_ENCODE_DEBUG_ARG, 98, msg.manufacturer);
pos = ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 106, msg.friendly_name);
#ifdef USE_VOICE_ASSISTANT
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 17, msg.voice_assistant_feature_flags);
+6 -6
View File
@@ -466,7 +466,7 @@ class HelloResponse final : public ProtoMessage {
#endif
uint32_t api_version_major{0};
uint32_t api_version_minor{0};
StringRef server_info{};
ProgmemStringRef server_info{};
StringRef name{};
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 {
@@ -608,17 +608,17 @@ class DeviceInfoResponse final : public ProtoMessage {
#endif
StringRef name{};
StringRef mac_address{};
StringRef esphome_version{};
ProgmemStringRef esphome_version{};
StringRef compilation_time{};
StringRef model{};
ProgmemStringRef model{};
#ifdef USE_DEEP_SLEEP
bool has_deep_sleep{false};
#endif
#ifdef ESPHOME_PROJECT_NAME
StringRef project_name{};
ProgmemStringRef project_name{};
#endif
#ifdef ESPHOME_PROJECT_NAME
StringRef project_version{};
ProgmemStringRef project_version{};
#endif
#ifdef USE_WEBSERVER
uint32_t webserver_port{0};
@@ -626,7 +626,7 @@ class DeviceInfoResponse final : public ProtoMessage {
#ifdef USE_BLUETOOTH_PROXY
uint32_t bluetooth_proxy_feature_flags{0};
#endif
StringRef manufacturer{};
ProgmemStringRef manufacturer{};
StringRef friendly_name{};
#ifdef USE_VOICE_ASSISTANT
uint32_t voice_assistant_feature_flags{0};
+6 -6
View File
@@ -970,7 +970,7 @@ const char *HelloResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("HelloResponse"));
dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major);
dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor);
dump_field(out, ESPHOME_PSTR("server_info"), this->server_info);
dump_progmem_string_field(out, ESPHOME_PSTR("server_info"), this->server_info);
dump_field(out, ESPHOME_PSTR("name"), this->name);
return out.c_str();
}
@@ -1021,17 +1021,17 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfoResponse"));
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("mac_address"), this->mac_address);
dump_field(out, ESPHOME_PSTR("esphome_version"), this->esphome_version);
dump_progmem_string_field(out, ESPHOME_PSTR("esphome_version"), this->esphome_version);
dump_field(out, ESPHOME_PSTR("compilation_time"), this->compilation_time);
dump_field(out, ESPHOME_PSTR("model"), this->model);
dump_progmem_string_field(out, ESPHOME_PSTR("model"), this->model);
#ifdef USE_DEEP_SLEEP
dump_field(out, ESPHOME_PSTR("has_deep_sleep"), this->has_deep_sleep);
#endif
#ifdef ESPHOME_PROJECT_NAME
dump_field(out, ESPHOME_PSTR("project_name"), this->project_name);
dump_progmem_string_field(out, ESPHOME_PSTR("project_name"), this->project_name);
#endif
#ifdef ESPHOME_PROJECT_NAME
dump_field(out, ESPHOME_PSTR("project_version"), this->project_version);
dump_progmem_string_field(out, ESPHOME_PSTR("project_version"), this->project_version);
#endif
#ifdef USE_WEBSERVER
dump_field(out, ESPHOME_PSTR("webserver_port"), this->webserver_port);
@@ -1039,7 +1039,7 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#ifdef USE_BLUETOOTH_PROXY
dump_field(out, ESPHOME_PSTR("bluetooth_proxy_feature_flags"), this->bluetooth_proxy_feature_flags);
#endif
dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer);
dump_progmem_string_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer);
dump_field(out, ESPHOME_PSTR("friendly_name"), this->friendly_name);
#ifdef USE_VOICE_ASSISTANT
dump_field(out, ESPHOME_PSTR("voice_assistant_feature_flags"), this->voice_assistant_feature_flags);
+12
View File
@@ -637,6 +637,18 @@ float ATM90E32Component::get_chip_temperature_() {
return (float) ctemp;
}
void ATM90E32Component::press_calibration_button(EntityBase *entity, void (ATM90E32Component::*action)(),
const LogString *hint, const LogString *hint2) {
ESP_LOGI(TAG, "%s", entity->get_name().c_str());
if (hint != nullptr) {
ESP_LOGI(TAG, "%s", LOG_STR_ARG(hint));
}
if (hint2 != nullptr) {
ESP_LOGI(TAG, "%s", LOG_STR_ARG(hint2));
}
(this->*action)();
}
void ATM90E32Component::run_gain_calibrations() {
const char *cs = this->get_calibration_id_();
if (!this->enable_gain_calibration_) {
+3
View File
@@ -139,6 +139,9 @@ class ATM90E32Component final : public PollingComponent,
int16_t calibrate_offset(uint8_t phase, bool voltage);
int16_t calibrate_power_offset(uint8_t phase, bool reactive);
void run_gain_calibrations();
// Body of the calibration buttons: logs the button name and the hint lines, then runs action.
void press_calibration_button(EntityBase *entity, void (ATM90E32Component::*action)(),
const LogString *hint = nullptr, const LogString *hint2 = nullptr);
#ifdef USE_NUMBER
void set_reference_voltage(uint8_t phase, number::Number *ref_voltage) { ref_voltages_[phase] = ref_voltage; }
void set_reference_current(uint8_t phase, number::Number *ref_current) { ref_currents_[phase] = ref_current; }
+58 -74
View File
@@ -4,7 +4,6 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
from esphome.types import ConfigType
from .. import atm90e32_ns
from ..sensor import ATM90E32Component
CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration"
@@ -14,83 +13,68 @@ CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration"
CONF_RUN_POWER_OFFSET_CALIBRATION = "run_power_offset_calibration"
CONF_CLEAR_POWER_OFFSET_CALIBRATION = "clear_power_offset_calibration"
ATM90E32GainCalibrationButton = atm90e32_ns.class_(
"ATM90E32GainCalibrationButton", button.Button
)
ATM90E32ClearGainCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearGainCalibrationButton", button.Button
)
ATM90E32OffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32OffsetCalibrationButton", button.Button
)
ATM90E32ClearOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearOffsetCalibrationButton", button.Button
)
ATM90E32PowerOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32PowerOffsetCalibrationButton", button.Button
)
ATM90E32ClearPowerOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearPowerOffsetCalibrationButton", button.Button
)
def _calibration_button(icon: str) -> cv.Schema:
return button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=icon
)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
cv.Optional(CONF_RUN_GAIN_CALIBRATION): button.button_schema(
ATM90E32GainCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:scale-balance",
),
cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): button.button_schema(
ATM90E32ClearGainCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:delete",
),
cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema(
ATM90E32OffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema(
ATM90E32ClearOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:delete",
),
cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): button.button_schema(
ATM90E32PowerOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): button.button_schema(
ATM90E32ClearPowerOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:delete",
),
cv.Optional(CONF_RUN_GAIN_CALIBRATION): _calibration_button("mdi:scale-balance"),
cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): _calibration_button("mdi:delete"),
cv.Optional(CONF_RUN_OFFSET_CALIBRATION): _calibration_button(ICON_SCALE),
cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): _calibration_button("mdi:delete"),
cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): _calibration_button(ICON_SCALE),
cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): _calibration_button("mdi:delete"),
}
def _press(action: str, hint: str | None = None, hint2: str | None = None) -> str:
"""press_calibration_button() logs at most two hint lines."""
args = "".join(
f", {cg.LogStringLiteral(h)}" for h in (hint, hint2) if h is not None
)
return f"press_calibration_button(entity, &{ATM90E32Component}::{action}{args})"
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_ID])
if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION):
b = await button.new_button(run_gain)
await cg.register_parented(b, parent)
if clear_gain := config.get(CONF_CLEAR_GAIN_CALIBRATION):
b = await button.new_button(clear_gain)
await cg.register_parented(b, parent)
if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION):
b = await button.new_button(run_offset)
await cg.register_parented(b, parent)
if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION):
b = await button.new_button(clear_offset)
await cg.register_parented(b, parent)
if run_power := config.get(CONF_RUN_POWER_OFFSET_CALIBRATION):
b = await button.new_button(run_power)
await cg.register_parented(b, parent)
if clear_power := config.get(CONF_CLEAR_POWER_OFFSET_CALIBRATION):
b = await button.new_button(clear_power)
await cg.register_parented(b, parent)
buttons = button.sub_buttons(config, parent=config[CONF_ID])
await buttons(
CONF_RUN_GAIN_CALIBRATION,
None,
apply=_press(
"run_gain_calibrations",
"[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values",
),
)
await buttons(
CONF_CLEAR_GAIN_CALIBRATION, None, apply=_press("clear_gain_calibrations")
)
await buttons(
CONF_RUN_OFFSET_CALIBRATION,
None,
apply=_press(
"run_offset_calibrations",
"[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**",
"[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save these values",
),
)
await buttons(
CONF_CLEAR_OFFSET_CALIBRATION, None, apply=_press("clear_offset_calibrations")
)
await buttons(
CONF_RUN_POWER_OFFSET_CALIBRATION,
None,
apply=_press(
"run_power_offset_calibrations",
"[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**",
"[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config file to save these values",
),
)
await buttons(
CONF_CLEAR_POWER_OFFSET_CALIBRATION,
None,
apply=_press("clear_power_offset_calibrations"),
)
@@ -1,77 +0,0 @@
#include "atm90e32_button.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
namespace esphome::atm90e32 {
ESPHOME_LOG_TAG(TAG, "atm90e32.button");
void ATM90E32GainCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Gain Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG,
"[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values");
this->parent_->run_gain_calibrations();
}
void ATM90E32ClearGainCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Gain button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_gain_calibrations();
}
void ATM90E32OffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Offset Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**");
ESP_LOGI(TAG, "[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save "
"these values");
this->parent_->run_offset_calibrations();
}
void ATM90E32ClearOffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Offset button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_offset_calibrations();
}
void ATM90E32PowerOffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Power Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**");
ESP_LOGI(TAG, "[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config "
"file to save these values");
this->parent_->run_power_offset_calibrations();
}
void ATM90E32ClearPowerOffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Power button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_power_offset_calibrations();
}
} // namespace esphome::atm90e32
@@ -1,57 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/atm90e32/atm90e32.h"
#include "esphome/components/button/button.h"
namespace esphome::atm90e32 {
class ATM90E32GainCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32GainCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32ClearGainCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32ClearGainCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32OffsetCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32OffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32ClearOffsetCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32ClearOffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32PowerOffsetCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32PowerOffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32ClearPowerOffsetCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32ClearPowerOffsetCalibrationButton() = default;
protected:
void press_action() override;
};
} // namespace esphome::atm90e32
+7 -24
View File
@@ -8,8 +8,8 @@ ESPHOME_LOG_TAG(TAG, "ballu.climate");
const uint16_t BALLU_HEADER_MARK = 9000;
const uint16_t BALLU_HEADER_SPACE = 4500;
const uint16_t BALLU_BIT_MARK = 575;
const uint16_t BALLU_ONE_SPACE = 1675;
const uint16_t BALLU_ZERO_SPACE = 550;
static constexpr remote_base::BitTimings BALLU_BIT_TIMINGS{
.bit_mark = BALLU_BIT_MARK, .one_space = 1675, .zero_space = 550};
const uint32_t BALLU_CARRIER_FREQUENCY = 38000;
@@ -104,13 +104,7 @@ void BalluClimate::transmit_state() {
data->mark(BALLU_HEADER_MARK);
data->space(BALLU_HEADER_SPACE);
// Data
for (uint8_t i : remote_state) {
for (uint8_t j = 0; j < 8; j++) {
data->mark(BALLU_BIT_MARK);
bool bit = i & (1 << j);
data->space(bit ? BALLU_ONE_SPACE : BALLU_ZERO_SPACE);
}
}
data->item_bytes<BALLU_BIT_TIMINGS>(remote_state, BALLU_STATE_LENGTH);
// Footer
data->mark(BALLU_BIT_MARK);
@@ -124,21 +118,10 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
uint8_t remote_state[BALLU_STATE_LENGTH] = {0};
// Read all bytes.
for (int i = 0; i < BALLU_STATE_LENGTH; i++) {
// Read bit
for (int j = 0; j < 8; j++) {
if (data.expect_item(BALLU_BIT_MARK, BALLU_ONE_SPACE)) {
remote_state[i] |= 1 << j;
} else if (!data.expect_item(BALLU_BIT_MARK, BALLU_ZERO_SPACE)) {
ESP_LOGV(TAG, "Byte %d bit %d fail", i, j);
return false;
}
}
ESP_LOGVV(TAG, "Byte %d %02X", i, remote_state[i]);
uint8_t remote_state[BALLU_STATE_LENGTH];
if (!data.expect_bytes<BALLU_BIT_TIMINGS>(remote_state, BALLU_STATE_LENGTH)) {
ESP_LOGV(TAG, "Data fail");
return false;
}
// Validate footer
if (!data.expect_mark(BALLU_BIT_MARK)) {
+1 -2
View File
@@ -43,5 +43,4 @@ async def register_bedjet_child(var: MockObj, config: ConfigType) -> None:
async def to_code(config: ConfigType) -> None:
var = await ble_client.new_ble_node_component(config)
await cg.add_variable(var.set_time_id, config.get(CONF_TIME_ID))
if (receive_timeout := config.get(CONF_RECEIVE_TIMEOUT)) is not None:
cg.add(var.set_status_timeout(receive_timeout))
cg.add_optional(var.set_status_timeout, config.get(CONF_RECEIVE_TIMEOUT))
@@ -111,8 +111,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
cg.add(var.set_pin(config[CONF_PIN]))
if CONF_MAX_REFRESH_RATE in config:
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
cg.add_optional(var.set_max_refresh_rate, config.get(CONF_MAX_REFRESH_RATE))
chipset = CHIPSETS[config[CONF_CHIPSET]]
cg.add(
+1
View File
@@ -122,6 +122,7 @@ float BL0940::calculate_energy_reference_() {
float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; }
void BL0940::reset_calibration() {
ESP_LOGI(TAG, "Resetting calibration defaults...");
#ifdef USE_NUMBER
if (this->current_calibration_number_ != nullptr && this->current_cal_ != 1) {
this->current_calibration_number_->make_call().set_value(0).perform();
+9 -18
View File
@@ -1,28 +1,19 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART
from esphome.types import ConfigType
from .. import CONF_BL0940_ID, bl0940_ns
from .. import CONF_BL0940_ID
from ..sensor import BL0940
CalibrationResetButton = bl0940_ns.class_(
"CalibrationResetButton", button.Button, cg.Component
)
CONFIG_SCHEMA = cv.All(
button.button_schema(
CalibrationResetButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART,
)
.extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)})
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART,
).extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)})
async def to_code(config: ConfigType) -> None:
var = await button.new_button(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_BL0940_ID])
await button.sub_buttons(config, parent=config[CONF_BL0940_ID]).create(
config, None, apply="reset_calibration()"
)
@@ -1,18 +0,0 @@
#include "calibration_reset_button.h"
#include "../bl0940.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::bl0940 {
ESPHOME_LOG_TAG(TAG, "bl0940.button.calibration_reset");
void CalibrationResetButton::dump_config() { LOG_BUTTON("", "Calibration Reset Button", this); }
void CalibrationResetButton::press_action() {
ESP_LOGI(TAG, "Resetting calibration defaults...");
this->parent_->reset_calibration();
}
} // namespace esphome::bl0940
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/button/button.h"
namespace esphome::bl0940 {
class BL0940; // Forward declaration of BL0940 class
class CalibrationResetButton final : public button::Button, public Component, public Parented<BL0940> {
public:
void dump_config() override;
void press_action() override;
};
} // namespace esphome::bl0940
@@ -83,8 +83,6 @@ async def to_code(config: ConfigType) -> None:
ibeacon_uuid = ble_device_base.as_reversed_hex_array(ibeacon_uuid)
cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:
cg.add(var.set_ibeacon_major(ibeacon_major))
cg.add_optional(var.set_ibeacon_major, config.get(CONF_IBEACON_MAJOR))
if (ibeacon_minor := config.get(CONF_IBEACON_MINOR)) is not None:
cg.add(var.set_ibeacon_minor(ibeacon_minor))
cg.add_optional(var.set_ibeacon_minor, config.get(CONF_IBEACON_MINOR))
+2 -4
View File
@@ -79,8 +79,6 @@ async def to_code(config: ConfigType) -> None:
ibeacon_uuid = ble_device_base.as_reversed_hex_array(ibeacon_uuid)
cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:
cg.add(var.set_ibeacon_major(ibeacon_major))
cg.add_optional(var.set_ibeacon_major, config.get(CONF_IBEACON_MAJOR))
if (ibeacon_minor := config.get(CONF_IBEACON_MINOR)) is not None:
cg.add(var.set_ibeacon_minor(ibeacon_minor))
cg.add_optional(var.set_ibeacon_minor, config.get(CONF_IBEACON_MINOR))
+7 -2
View File
@@ -23,6 +23,7 @@ from esphome.const import (
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
ApplyEntity,
SubEntities,
entity_duplicate_validator,
queue_entity_register,
@@ -46,6 +47,7 @@ DEVICE_CLASSES = [
button_ns = cg.esphome_ns.namespace("button")
Button = button_ns.class_("Button", cg.EntityBase)
ButtonPtr = Button.operator("ptr")
ApplyButton = button_ns.class_("ApplyButton", Button)
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
@@ -155,11 +157,14 @@ async def new_button(config: ConfigType, *args: SafeExpType) -> MockObj:
return var
APPLY_ENTITY = ApplyEntity(ApplyButton, ((ButtonPtr, "entity"),))
def sub_buttons(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_button."""
return SubEntities(new_button, config, parent)
"""Return a SubEntities bound to new_button; ``apply`` statements can use ``entity``."""
return SubEntities(new_button, config, parent, APPLY_ENTITY)
BUTTON_PRESS_SCHEMA = maybe_simple_id(
+10
View File
@@ -46,4 +46,14 @@ class Button : public EntityBase {
LazyCallbackManager<void()> press_callback_{};
};
/// Calls the codegen-generated Fn(this) on a press; the hub and any publish are baked into Fn.
template<auto Fn> class ApplyButton final : public Button {
public:
// Not "= default": placement new would zero-fill .bss that is already zero.
ApplyButton() {}
protected:
void press_action() override { Fn(this); }
};
} // namespace esphome::button
@@ -58,8 +58,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
request->send(stream);
}
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
const auto &ssid = request->arg("ssid");
const auto &psk = request->arg("psk");
const auto &ssid = request->arg(ESPHOME_F("ssid"));
const auto &psk = request->arg(ESPHOME_F("psk"));
ESP_LOGI(TAG,
"Requested WiFi Settings Change:\n"
" SSID='%s'\n"
@@ -117,11 +117,7 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
// All other requests get the captive portal page
// This includes OS captive portal detection endpoints which will trigger
// the captive portal when they don't receive their expected responses
#ifndef USE_ESP8266
auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
#else
auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
#endif
#ifdef USE_CAPTIVE_PORTAL_GZIP
response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
#else
+1 -2
View File
@@ -69,8 +69,7 @@ async def to_code(config: ConfigType) -> None:
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_VERSION, var.set_version)
if (baseline := config.get(CONF_BASELINE)) is not None:
cg.add(var.set_baseline(baseline))
cg.add_optional(var.set_baseline, config.get(CONF_BASELINE))
await cg.add_variable(var.set_temperature, config.get(CONF_TEMPERATURE))
await cg.add_variable(var.set_humidity, config.get(CONF_HUMIDITY))
+2 -2
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.const import CONF_SOURCES
import esphome.config_validation as cv
from esphome.const import (
CONF_ACCURACY_DECIMALS,
@@ -16,7 +17,7 @@ from esphome.const import (
from esphome.core.entity_helpers import inherit_property_from
from esphome.types import ConfigType
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
CODEOWNERS = ["@kahrendt"]
combination_ns = cg.esphome_ns.namespace("combination")
@@ -59,7 +60,6 @@ CONF_MEDIAN = "median"
CONF_MIN = "min"
CONF_MOST_RECENTLY_UPDATED = "most_recently_updated"
CONF_PROCESS_STD_DEV = "process_std_dev"
CONF_SOURCES = "sources"
CONF_STD_DEV = "std_dev"
+2
View File
@@ -65,10 +65,12 @@ CONF_REMOTE_ID = "remote_id"
CONF_REQUEST_HEADERS = "request_headers"
CONF_ROLE = "role"
CONF_ROWS = "rows"
CONF_RST_PIN = "rst_pin"
CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_SERIAL_NUMBER = "serial_number"
CONF_SHA256 = "sha256"
CONF_SLOT = "slot"
CONF_SOURCES = "sources"
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
CONF_STOP_BITS = "stop_bits"
CONF_TARGET_COUNT = "target_count"
+7 -20
View File
@@ -1,29 +1,18 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID,
CONF_SOURCE_ID,
)
from esphome.core.entity_helpers import inherit_property_from
from esphome.types import ConfigType
from .. import copy_ns
CopyButton = copy_ns.class_("CopyButton", button.Button, cg.Component)
CONFIG_SCHEMA = (
button.button_schema(CopyButton)
.extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button),
}
)
.extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = button.button_schema(button.ApplyButton).extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button),
}
)
FINAL_VALIDATE_SCHEMA = cv.All(
@@ -34,8 +23,6 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await button.register_button(var, config)
await cg.register_component(var, config)
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
await button.sub_buttons(config, parent=config[CONF_SOURCE_ID]).create(
config, None, apply="press()"
)
@@ -1,12 +0,0 @@
#include "copy_button.h"
#include "esphome/core/log.h"
namespace esphome::copy {
ESPHOME_LOG_TAG(TAG, "copy.button");
void CopyButton::dump_config() { LOG_BUTTON("", "Copy Button", this); }
void CopyButton::press_action() { source_->press(); }
} // namespace esphome::copy
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/button/button.h"
namespace esphome::copy {
class CopyButton final : public button::Button, public Component {
public:
void set_source(button::Button *source) { source_ = source; }
void dump_config() override;
protected:
void press_action() override;
button::Button *source_;
};
} // namespace esphome::copy
@@ -5,6 +5,10 @@
namespace esphome::cst9220 {
// Id 0 is the base Touchscreen class touch timeout.
static constexpr uint32_t REREAD_TIMEOUT_ID = 1;
static constexpr uint32_t REREAD_DELAY_MS = 20;
void CST9220Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
@@ -92,15 +96,15 @@ void CST9220Touchscreen::update_touches() {
return;
}
// Acknowledge the report so the controller can prepare the next one.
uint8_t ack = TOUCH_ACK;
this->write_register16(REG_TOUCH_DATA, &ack, 1);
// A valid report carries the ACK marker at offset 6; offset 0 holds the first
// point and must be neither the ACK marker nor empty. Anything else means no
// valid touch data this cycle, which we report as zero touches (not a skip).
if (data[0] == TOUCH_ACK || data[0] == 0x00 || data[6] != TOUCH_ACK)
// A valid report carries the marker at offset 6. Writing an acknowledge back makes the controller return
// invalid frames during a long press, so none is written. An invalid frame keeps the previous points; while
// a finger is down, read again shortly so a lost release frame does not leave the touch held.
if (data[6] != TOUCH_ACK) {
this->skip_update_ = true;
if (!this->touches_.empty())
this->set_timeout(REREAD_TIMEOUT_ID, REREAD_DELAY_MS, [this] { this->store_.touched = true; });
return;
}
uint8_t num_touches = data[5] & 0x7F;
if (num_touches > CST9220_MAX_TOUCHES)
+1 -2
View File
@@ -117,8 +117,7 @@ async def to_code(config: ConfigType) -> None:
)
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))
if (max_duration := config.get(CONF_MAX_DURATION)) is not None:
cg.add(var.set_max_duration(max_duration))
cg.add_optional(var.set_max_duration, config.get(CONF_MAX_DURATION))
cg.add(var.set_malfunction_detection(config[CONF_MALFUNCTION_DETECTION]))
if malfunction_action := config.get(CONF_MALFUNCTION_ACTION):
await automation.build_automation(
+24 -56
View File
@@ -9,6 +9,14 @@ ESPHOME_LOG_TAG(TAG, "daikin.climate");
static constexpr uint32_t BOOST_TIMEOUT_ID = 0;
static constexpr uint8_t DAIKIN_HEADER_BYTE0 = 0x11;
static constexpr uint8_t DAIKIN_HEADER_BYTE1 = 0xDA;
static constexpr uint8_t DAIKIN_HEADER_BYTE2 = 0x27;
static constexpr uint8_t DAIKIN_HEADER_BYTE3 = 0x00;
static constexpr uint8_t DAIKIN_FRAME_TYPE_INDEX = 4;
static constexpr uint8_t DAIKIN_FRAME_HEADER_SIZE = DAIKIN_FRAME_TYPE_INDEX + 1;
static constexpr uint8_t DAIKIN_STATE_FRAME_OFFSET = 2 * DAIKIN_COMFORT_FRAME_SIZE;
void DaikinClimate::setup() {
climate_ir::ClimateIR::setup();
// A boost preset restored from flash gets its timer too
@@ -42,37 +50,19 @@ void DaikinClimate::transmit_state() {
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (int i = 0; i < 8; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = remote_state[i] & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_state, DAIKIN_COMFORT_FRAME_SIZE);
data->mark(DAIKIN_BIT_MARK);
data->space(DAIKIN_MESSAGE_SPACE);
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (int i = 8; i < 16; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = remote_state[i] & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_state + DAIKIN_COMFORT_FRAME_SIZE, DAIKIN_COMFORT_FRAME_SIZE);
data->mark(DAIKIN_BIT_MARK);
data->space(DAIKIN_MESSAGE_SPACE);
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (int i = 16; i < 35; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = remote_state[i] & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_state + DAIKIN_STATE_FRAME_OFFSET, DAIKIN_STATE_FRAME_SIZE);
data->mark(DAIKIN_BIT_MARK);
data->space(0);
@@ -284,46 +274,24 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
}
bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t frame[DAIKIN_STATE_FRAME_SIZE] = {};
uint8_t frame[DAIKIN_STATE_FRAME_SIZE];
if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
return false;
}
// Frame header, then the frame type that sets the frame size
if (!data.expect_bytes<DAIKIN_BIT_TIMINGS>(frame, DAIKIN_FRAME_HEADER_SIZE) || frame[0] != DAIKIN_HEADER_BYTE0 ||
frame[1] != DAIKIN_HEADER_BYTE1 || frame[2] != DAIKIN_HEADER_BYTE2 || frame[3] != DAIKIN_HEADER_BYTE3) {
return false;
}
uint8_t frame_size = DAIKIN_STATE_FRAME_SIZE;
for (uint8_t pos = 0; pos < frame_size; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) {
return false;
}
}
frame[pos] = byte;
if (pos == 0) {
// frame header
if (byte != 0x11)
return false;
} else if (pos == 1) {
// frame header
if (byte != 0xDA)
return false;
} else if (pos == 2) {
// frame header
if (byte != 0x27)
return false;
} else if (pos == 3) { // NOLINT(bugprone-branch-clone)
// frame header
if (byte != 0x00)
return false;
} else if (pos == 4) {
// frame type
if (byte == DAIKIN_FRAME_TYPE_COMFORT) {
frame_size = DAIKIN_COMFORT_FRAME_SIZE;
} else if (byte != DAIKIN_FRAME_TYPE_STATE) {
return false;
}
}
if (frame[DAIKIN_FRAME_TYPE_INDEX] == DAIKIN_FRAME_TYPE_COMFORT) {
frame_size = DAIKIN_COMFORT_FRAME_SIZE;
} else if (frame[DAIKIN_FRAME_TYPE_INDEX] != DAIKIN_FRAME_TYPE_STATE) {
return false;
}
if (!data.expect_bytes<DAIKIN_BIT_TIMINGS>(frame + DAIKIN_FRAME_HEADER_SIZE, frame_size - DAIKIN_FRAME_HEADER_SIZE)) {
return false;
}
if (frame[frame_size - 1] != sum_bytes(frame, frame_size - 1)) {
return false;
+2
View File
@@ -45,6 +45,8 @@ const uint32_t DAIKIN_HEADER_SPACE = 1760;
const uint32_t DAIKIN_BIT_MARK = 520;
const uint32_t DAIKIN_ONE_SPACE = 1370;
const uint32_t DAIKIN_ZERO_SPACE = 360;
static constexpr remote_base::BitTimings DAIKIN_BIT_TIMINGS{
.bit_mark = DAIKIN_BIT_MARK, .one_space = DAIKIN_ONE_SPACE, .zero_space = DAIKIN_ZERO_SPACE};
const uint32_t DAIKIN_MESSAGE_SPACE = 32300;
// Frame types (byte 4) and sizes
+8 -31
View File
@@ -39,13 +39,7 @@ void DaikinArcClimate::transmit_query_() {
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (uint8_t i : remote_header) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = i & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_header, sizeof(remote_header));
data->mark(DAIKIN_BIT_MARK);
data->space(0);
@@ -119,26 +113,14 @@ void DaikinArcClimate::transmit_state() {
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (uint8_t i : remote_header) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = i & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_header, sizeof(remote_header));
data->mark(DAIKIN_BIT_MARK);
data->space(DAIKIN_MESSAGE_SPACE);
data->mark(DAIKIN_HEADER_MARK);
data->space(DAIKIN_HEADER_SPACE);
for (uint8_t i : remote_state) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BIT_MARK);
bool bit = i & mask;
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
}
}
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_state, sizeof(remote_state));
data->mark(DAIKIN_BIT_MARK);
data->space(0);
@@ -339,7 +321,7 @@ bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
}
bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {};
uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE];
bool valid_daikin_frame = false;
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
@@ -418,16 +400,11 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
}
for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) {
ESP_LOGI(TAG, "non daikin_arc expect item pos: %d", pos);
return false;
}
if (!data.expect_bytes<DAIKIN_BIT_TIMINGS>(&state_frame[pos], 1)) {
ESP_LOGI(TAG, "non daikin_arc expect item pos: %d", pos);
return false;
}
state_frame[pos] = byte;
const uint8_t byte = state_frame[pos];
if (pos == 0) {
// frame header
if (byte != 0x11) {
@@ -36,6 +36,8 @@ const uint32_t DAIKIN_HEADER_SPACE = 1760;
const uint32_t DAIKIN_BIT_MARK = 400;
const uint32_t DAIKIN_ONE_SPACE = 1300;
const uint32_t DAIKIN_ZERO_SPACE = 480;
static constexpr remote_base::BitTimings DAIKIN_BIT_TIMINGS{
.bit_mark = DAIKIN_BIT_MARK, .one_space = DAIKIN_ONE_SPACE, .zero_space = DAIKIN_ZERO_SPACE};
const uint32_t DAIKIN_MESSAGE_SPACE = 35000;
const uint8_t DAIKIN_DBG_TOLERANCE = 25;
+16 -46
View File
@@ -6,6 +6,12 @@ namespace esphome::daikin_brc {
ESPHOME_LOG_TAG(TAG, "daikin_brc.climate");
static constexpr uint8_t DAIKIN_BRC_HEADER_BYTE0 = 0x11;
static constexpr uint8_t DAIKIN_BRC_HEADER_BYTE1 = 0xDA;
static constexpr uint8_t DAIKIN_BRC_HEADER_BYTE2 = 0x17;
static constexpr uint8_t DAIKIN_BRC_HEADER_BYTE3 = 0x18;
static constexpr uint8_t DAIKIN_BRC_FRAME_TYPE_STATE = 0x00;
void DaikinBrcClimate::control(const climate::ClimateCall &call) {
this->mode_button_ = 0x00;
if (call.get_mode().has_value()) {
@@ -42,25 +48,14 @@ void DaikinBrcClimate::transmit_state() {
data->mark(DAIKIN_BRC_HEADER_MARK);
data->space(DAIKIN_BRC_HEADER_SPACE);
for (int i = 0; i < DAIKIN_BRC_PREAMBLE_SIZE; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BRC_BIT_MARK);
bool bit = remote_state[i] & mask;
data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE);
}
}
data->item_bytes<DAIKIN_BRC_BIT_TIMINGS>(remote_state, DAIKIN_BRC_PREAMBLE_SIZE);
data->mark(DAIKIN_BRC_BIT_MARK);
data->space(DAIKIN_BRC_MESSAGE_SPACE);
data->mark(DAIKIN_BRC_HEADER_MARK);
data->space(DAIKIN_BRC_HEADER_SPACE);
for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE; i++) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DAIKIN_BRC_BIT_MARK);
bool bit = remote_state[i] & mask;
data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE);
}
}
data->item_bytes<DAIKIN_BRC_BIT_TIMINGS>(remote_state + DAIKIN_BRC_PREAMBLE_SIZE,
DAIKIN_BRC_TRANSMIT_FRAME_SIZE - DAIKIN_BRC_PREAMBLE_SIZE);
data->mark(DAIKIN_BRC_BIT_MARK);
data->space(0);
@@ -232,41 +227,16 @@ bool DaikinBrcClimate::parse_state_frame_(const uint8_t frame[]) {
}
bool DaikinBrcClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE] = {};
uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE];
if (!data.expect_item(DAIKIN_BRC_HEADER_MARK, DAIKIN_BRC_HEADER_SPACE)) {
return false;
}
for (uint8_t pos = 0; pos < DAIKIN_BRC_STATE_FRAME_SIZE; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ZERO_SPACE)) {
return false;
}
}
state_frame[pos] = byte;
if (pos == 0) {
// frame header
if (byte != 0x11)
return false;
} else if (pos == 1) {
// frame header
if (byte != 0xDA)
return false;
} else if (pos == 2) {
// frame header
if (byte != 0x17)
return false;
} else if (pos == 3) {
// frame header, with this unit's address in the low 3 bits
if (byte != (0x18 | this->sub_unit_))
return false;
} else if (pos == 4) {
// frame type
if (byte != 0x00)
return false;
}
// Frame header, with this unit's address in the low 3 bits of byte 3, then the frame type
if (!data.expect_bytes<DAIKIN_BRC_BIT_TIMINGS>(state_frame, DAIKIN_BRC_STATE_FRAME_SIZE) ||
state_frame[0] != DAIKIN_BRC_HEADER_BYTE0 || state_frame[1] != DAIKIN_BRC_HEADER_BYTE1 ||
state_frame[2] != DAIKIN_BRC_HEADER_BYTE2 || state_frame[3] != (DAIKIN_BRC_HEADER_BYTE3 | this->sub_unit_) ||
state_frame[4] != DAIKIN_BRC_FRAME_TYPE_STATE) {
return false;
}
return this->parse_state_frame_(state_frame);
}
+2 -2
View File
@@ -31,8 +31,8 @@ const uint32_t DAIKIN_BRC_IR_FREQUENCY = 38000;
const uint32_t DAIKIN_BRC_HEADER_MARK = 5070;
const uint32_t DAIKIN_BRC_HEADER_SPACE = 2140;
const uint32_t DAIKIN_BRC_BIT_MARK = 370;
const uint32_t DAIKIN_BRC_ONE_SPACE = 1780;
const uint32_t DAIKIN_BRC_ZERO_SPACE = 710;
static constexpr remote_base::BitTimings DAIKIN_BRC_BIT_TIMINGS{
.bit_mark = DAIKIN_BRC_BIT_MARK, .one_space = 1780, .zero_space = 710};
const uint32_t DAIKIN_BRC_MESSAGE_SPACE = 29410;
const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_F = 72; // Dry/Fan mode is always 17 Celsius.
+3 -6
View File
@@ -364,8 +364,7 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CONF_SLEEP_DURATION in config:
cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION]))
cg.add_optional(var.set_sleep_duration, config.get(CONF_SLEEP_DURATION))
if (wakeup_pins := config.get(CONF_WAKEUP_PIN)) is not None:
if CORE.is_bk72xx:
cg.add(var.init_wakeup_pins(len(wakeup_pins)))
@@ -380,8 +379,7 @@ async def to_code(config: ConfigType) -> None:
)
else:
await cg.add_pin(var.set_wakeup_pin, wakeup_pins[0][CONF_PIN])
if CONF_WAKEUP_PIN_MODE in config:
cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE]))
cg.add_optional(var.set_wakeup_pin_mode, config.get(CONF_WAKEUP_PIN_MODE))
if CONF_RUN_DURATION in config:
run_duration_config = config[CONF_RUN_DURATION]
if not isinstance(run_duration_config, dict):
@@ -427,8 +425,7 @@ async def to_code(config: ConfigType) -> None:
await automation.build_automation(trigger, [(WakeupCause, "cause")], wake_conf)
cg.add_define("USE_DEEP_SLEEP_ON_WAKE")
if CONF_TOUCH_WAKEUP in config:
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
cg.add_optional(var.set_touch_wakeup, config.get(CONF_TOUCH_WAKEUP))
if CORE.using_zephyr:
# Devices are suspended when CPU is entering a low power state
# https://github.com/nrfconnect/sdk-zephyr/blob/v3.7.99-ncs2-2/doc/services/pm/device.rst#system-managed-device-power-management
+5 -25
View File
@@ -23,13 +23,7 @@ void DelonghiClimate::transmit_state() {
data->mark(DELONGHI_HEADER_MARK);
data->space(DELONGHI_HEADER_SPACE);
for (unsigned char b : remote_state) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DELONGHI_BIT_MARK);
bool bit = b & mask;
data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE);
}
}
data->item_bytes<DELONGHI_BIT_TIMINGS>(remote_state, DELONGHI_STATE_FRAME_SIZE);
data->mark(DELONGHI_BIT_MARK);
data->space(0);
@@ -155,27 +149,13 @@ bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
}
bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) {
uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {};
uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE];
if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) {
return false;
}
for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) {
byte |= 1 << bit;
} else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) {
return false;
}
}
state_frame[pos] = byte;
if (pos == 0) {
// frame header
if (byte != DELONGHI_ADDRESS) {
return false;
}
}
}
if (!data.expect_bytes<DELONGHI_BIT_TIMINGS>(state_frame, DELONGHI_STATE_FRAME_SIZE) ||
state_frame[0] != DELONGHI_ADDRESS)
return false;
return this->parse_state_frame_(state_frame);
}
+2 -2
View File
@@ -33,8 +33,8 @@ const uint32_t DELONGHI_IR_FREQUENCY = 38000;
const uint32_t DELONGHI_HEADER_MARK = 9000;
const uint32_t DELONGHI_HEADER_SPACE = 4500;
const uint32_t DELONGHI_BIT_MARK = 465;
const uint32_t DELONGHI_ONE_SPACE = 1750;
const uint32_t DELONGHI_ZERO_SPACE = 670;
static constexpr remote_base::BitTimings DELONGHI_BIT_TIMINGS{
.bit_mark = DELONGHI_BIT_MARK, .one_space = 1750, .zero_space = 670};
// State Frame size
const uint8_t DELONGHI_STATE_FRAME_SIZE = 8;
@@ -5,15 +5,13 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
from esphome.types import ConfigType
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns
OutPinLevelSelect = dfrobot_sen0609_ns.class_("OutPinLevelSelect", select.Select)
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema(
OutPinLevelSelect,
select.ApplySelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
@@ -25,5 +23,8 @@ async def to_code(config: ConfigType) -> None:
selects = select.sub_selects(config, parent=hub)
# The level the OUT pin has while a target is present; the index is the module's value
await selects(
CONF_OUT_PIN_LEVEL, hub.set_out_pin_level_select, options=["low", "high"]
CONF_OUT_PIN_LEVEL,
hub.set_out_pin_level_select,
options=["low", "high"],
apply="control_out_pin_level(index)",
)
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class OutPinLevelSelect final : public select::Select, public Parented<DFRobotSEN0609Component> {
public:
// User provided, not "= default": `new(p) OutPinLevelSelect()` would zero-fill .bss that is already zero.
OutPinLevelSelect() {}
protected:
void control(size_t index) override { this->parent_->control_out_pin_level(index); }
};
} // namespace esphome::dfrobot_sen0609
@@ -18,7 +18,6 @@ from .. import (
final_validate_mode,
)
DFRobotSEN0609Switch = dfrobot_sen0609_ns.class_("DFRobotSEN0609Switch", switch.Switch)
SwitchSetting = dfrobot_sen0609_ns.enum("SwitchSetting")
CONF_MICRO_MOTION = "micro_motion"
@@ -29,7 +28,7 @@ CONF_RUN_LED = "run_led"
def _led() -> cv.Schema:
# The module forgets the LED state at power off, so it is restored from here
return switch.switch_schema(
DFRobotSEN0609Switch,
switch.ApplySwitch,
default_restore_mode="RESTORE_DEFAULT_ON",
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
@@ -41,7 +40,7 @@ CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_MICRO_MOTION): switch.switch_schema(
DFRobotSEN0609Switch,
switch.ApplySwitch,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
@@ -64,5 +63,7 @@ async def to_code(config: ConfigType) -> None:
(CONF_OUT_LED, SwitchSetting.SWITCH_OUT_LED),
):
await switches(
key, lambda s, setting=setting: hub.set_switch(setting, s), setting
key,
lambda s, setting=setting: hub.set_switch(setting, s),
apply=f"control_switch({setting}, state)",
)
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Switch final : public switch_::Switch, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Switch(SwitchSetting setting) : setting_(setting) {}
protected:
void write_state(bool state) override { this->parent_->control_switch(this->setting_, state); }
SwitchSetting setting_;
};
} // namespace esphome::dfrobot_sen0609
@@ -277,8 +277,7 @@ async def menu_item_to_code(menu, config, parent):
if CONF_ITEMS in config:
for c in config[CONF_ITEMS]:
await menu_item_to_code(menu, c, item)
if CONF_IMMEDIATE_EDIT in config:
cg.add(item.set_immediate_edit(config[CONF_IMMEDIATE_EDIT]))
cg.add_optional(item.set_immediate_edit, config.get(CONF_IMMEDIATE_EDIT))
if config[CONF_TYPE] == CONF_SELECT:
await cg.add_variable(item.set_select_variable, config[CONF_SELECT])
if config[CONF_TYPE] == CONF_NUMBER:
+7 -14
View File
@@ -91,20 +91,13 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_active_pullup(config[CONF_ACTIVE_PULLUP]))
cg.add(var.set_channel_count(get_channel_count(config)))
if CONF_BUS_SLEEP in config:
cg.add(var.set_bus_sleep(config[CONF_BUS_SLEEP]))
if CONF_HUB_SLEEP in config:
cg.add(var.set_hub_sleep(config[CONF_HUB_SLEEP]))
cg.add_optional(var.set_bus_sleep, config.get(CONF_BUS_SLEEP))
cg.add_optional(var.set_hub_sleep, config.get(CONF_HUB_SLEEP))
if CONF_RESET_LOW_TIME in config:
cg.add(var.set_val_trstl(config[CONF_RESET_LOW_TIME]))
if CONF_MASTER_SAMPLE_TIME in config:
cg.add(var.set_val_tmsp(config[CONF_MASTER_SAMPLE_TIME]))
if CONF_WRITE_0_LOW_TIME in config:
cg.add(var.set_val_tw0l(config[CONF_WRITE_0_LOW_TIME]))
if CONF_RECOVERY_TIME in config:
cg.add(var.set_val_trec0(config[CONF_RECOVERY_TIME]))
if CONF_ACTIVE_PULLUP_RESISTANCE in config:
cg.add(var.set_val_rwpu(config[CONF_ACTIVE_PULLUP_RESISTANCE]))
cg.add_optional(var.set_val_trstl, config.get(CONF_RESET_LOW_TIME))
cg.add_optional(var.set_val_tmsp, config.get(CONF_MASTER_SAMPLE_TIME))
cg.add_optional(var.set_val_tw0l, config.get(CONF_WRITE_0_LOW_TIME))
cg.add_optional(var.set_val_trec0, config.get(CONF_RECOVERY_TIME))
cg.add_optional(var.set_val_rwpu, config.get(CONF_ACTIVE_PULLUP_RESISTANCE))
await cg.add_pin(var.set_sleep_pin, config.get(CONF_SLEEP_PIN))
+1 -2
View File
@@ -55,5 +55,4 @@ async def to_code(config: ConfigType) -> None:
var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
)
if CONF_MAX_DURATION in config:
cg.add(var.set_max_duration(config[CONF_MAX_DURATION]))
cg.add_optional(var.set_max_duration, config.get(CONF_MAX_DURATION))
+1 -2
View File
@@ -252,8 +252,7 @@ async def to_code(config):
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
cg.add(var.set_enable_pins(enable))
cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
if CONF_RESET_DURATION in config:
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
cg.add_optional(var.set_reset_duration, config.get(CONF_RESET_DURATION))
if transform := config.get(CONF_TRANSFORM):
transform[CONF_SWAP_XY] = False
else:
+1 -2
View File
@@ -265,8 +265,7 @@ async def esp32_pin_to_code(config):
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
if config[CONF_INVERTED]:
cg.add(var.set_inverted(True))
if CONF_DRIVE_STRENGTH in config:
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
cg.add_optional(var.set_drive_strength, config.get(CONF_DRIVE_STRENGTH))
flags = pins.gpio_flags_expr(config[CONF_MODE])
if config[CONF_HOLD_STATE]:
flags = flags | cg.gpio_Flags.FLAG_HOLD
+4 -8
View File
@@ -535,16 +535,12 @@ async def to_code(config: ConfigType) -> None:
):
cg.add_define("ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS", None)
if CONF_AUTH_REQ_MODE in config:
cg.add(var.set_auth_req(config[CONF_AUTH_REQ_MODE]))
if CONF_MAX_KEY_SIZE in config:
cg.add(var.set_max_key_size(config[CONF_MAX_KEY_SIZE]))
if CONF_MIN_KEY_SIZE in config:
cg.add(var.set_min_key_size(config[CONF_MIN_KEY_SIZE]))
cg.add_optional(var.set_auth_req, config.get(CONF_AUTH_REQ_MODE))
cg.add_optional(var.set_max_key_size, config.get(CONF_MAX_KEY_SIZE))
cg.add_optional(var.set_min_key_size, config.get(CONF_MIN_KEY_SIZE))
cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME]))
if (name := config.get(CONF_NAME)) is not None:
cg.add(var.set_name(name))
cg.add_optional(var.set_name, config.get(CONF_NAME))
await cg.register_component(var, config)
# Define max connections for use in C++ code (e.g., ble_server.h)
@@ -83,8 +83,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
# TX power control only available on native Bluetooth (not ESP-Hosted)
if CONF_TX_POWER in config:
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
cg.add_optional(var.set_tx_power, config.get(CONF_TX_POWER))
cg.add_define("USE_ESP32_BLE_ADVERTISING")
@@ -647,8 +647,7 @@ async def to_code(config):
)
)
)
if CONF_MANUFACTURER_DATA in config:
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
cg.add_optional(var.set_manufacturer_data, config.get(CONF_MANUFACTURER_DATA))
for service_config in config[CONF_SERVICES]:
# Calculate the optimal number of handles based on the number of characteristics and descriptors
num_handles = calculate_num_handles(service_config)
+2 -4
View File
@@ -157,10 +157,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_rx(config[CONF_RX_PIN]))
cg.add(var.set_tx(config[CONF_TX_PIN]))
cg.add(var.set_mode(config[CONF_MODE]))
if (rx_queue_len := config.get(CONF_RX_QUEUE_LEN)) is not None:
cg.add(var.set_rx_queue_len(rx_queue_len))
if (tx_queue_len := config.get(CONF_TX_QUEUE_LEN)) is not None:
cg.add(var.set_tx_queue_len(tx_queue_len))
cg.add_optional(var.set_rx_queue_len, config.get(CONF_RX_QUEUE_LEN))
cg.add_optional(var.set_tx_queue_len, config.get(CONF_TX_QUEUE_LEN))
if CONF_TX_ENQUEUE_TIMEOUT in config:
tx_enqueue_timeout_ms = config[CONF_TX_ENQUEUE_TIMEOUT].total_milliseconds
@@ -170,10 +170,8 @@ async def to_code(config: ConfigType) -> None:
await power_management.register_power_management(var, config)
add_idf_sdkconfig_option("CONFIG_PM_ENABLE", True)
if (max_mhz := config.get(CONF_MAX_FREQUENCY)) is not None:
cg.add(var.set_max_freq_mhz(max_mhz))
if (min_mhz := config.get(CONF_MIN_FREQUENCY)) is not None:
cg.add(var.set_min_freq_mhz(min_mhz))
cg.add_optional(var.set_max_freq_mhz, config.get(CONF_MAX_FREQUENCY))
cg.add_optional(var.set_min_freq_mhz, config.get(CONF_MIN_FREQUENCY))
if config[CONF_PROFILING]:
add_idf_sdkconfig_option("CONFIG_PM_PROFILING", True)
if config[CONF_TRACE]:
@@ -137,8 +137,7 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_PIN][CONF_INVERTED]:
cg.add(var.set_inverted(True))
if CONF_MAX_REFRESH_RATE in config:
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
cg.add_optional(var.set_max_refresh_rate, config.get(CONF_MAX_REFRESH_RATE))
if CONF_CHIPSET in config:
chipset = CHIPSETS[config[CONF_CHIPSET]]
@@ -169,5 +168,4 @@ async def to_code(config: ConfigType) -> None:
)
cg.add(var.set_use_psram(config[CONF_USE_PSRAM]))
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
if CONF_USE_DMA in config:
cg.add(var.set_use_dma(config[CONF_USE_DMA]))
cg.add_optional(var.set_use_dma, config.get(CONF_USE_DMA))
+7 -14
View File
@@ -366,20 +366,13 @@ async def to_code(config: ConfigType) -> None:
cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER]))
if variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4):
if CONF_FILTER_MODE in config:
cg.add(touch.set_filter_mode(config[CONF_FILTER_MODE]))
if CONF_DEBOUNCE_COUNT in config:
cg.add(touch.set_debounce_count(config[CONF_DEBOUNCE_COUNT]))
if CONF_NOISE_THRESHOLD in config:
cg.add(touch.set_noise_threshold(config[CONF_NOISE_THRESHOLD]))
if CONF_JITTER_STEP in config:
cg.add(touch.set_jitter_step(config[CONF_JITTER_STEP]))
if CONF_SMOOTH_MODE in config:
cg.add(touch.set_smooth_level(config[CONF_SMOOTH_MODE]))
if CONF_DENOISE_GRADE in config:
cg.add(touch.set_denoise_grade(config[CONF_DENOISE_GRADE]))
if CONF_DENOISE_CAP_LEVEL in config:
cg.add(touch.set_denoise_cap(config[CONF_DENOISE_CAP_LEVEL]))
cg.add_optional(touch.set_filter_mode, config.get(CONF_FILTER_MODE))
cg.add_optional(touch.set_debounce_count, config.get(CONF_DEBOUNCE_COUNT))
cg.add_optional(touch.set_noise_threshold, config.get(CONF_NOISE_THRESHOLD))
cg.add_optional(touch.set_jitter_step, config.get(CONF_JITTER_STEP))
cg.add_optional(touch.set_smooth_level, config.get(CONF_SMOOTH_MODE))
cg.add_optional(touch.set_denoise_grade, config.get(CONF_DENOISE_GRADE))
cg.add_optional(touch.set_denoise_cap, config.get(CONF_DENOISE_CAP_LEVEL))
if CONF_WATERPROOF_GUARD_RING in config:
cg.add(
touch.set_waterproof_guard_ring_pad(config[CONF_WATERPROOF_GUARD_RING])
+2 -4
View File
@@ -151,10 +151,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_traffic_generator_target_ip(target_ip))
if (group := config.get(CONF_CSI_TRAFFIC_MULTICAST_GROUP)) is not None:
cg.add(var.set_csi_traffic_multicast_group(group))
if (on_hits := config.get(CONF_MOTION_ON_HITS)) is not None:
cg.add(var.set_motion_on_hits(on_hits))
if (off_hits := config.get(CONF_MOTION_OFF_HITS)) is not None:
cg.add(var.set_motion_off_hits(off_hits))
cg.add_optional(var.set_motion_on_hits, config.get(CONF_MOTION_ON_HITS))
cg.add_optional(var.set_motion_off_hits, config.get(CONF_MOTION_OFF_HITS))
if esp32.get_esp32_variant() == VARIANT_ESP32C5:
band = CORE.config[WIFI_DOMAIN].get(wifi.CONF_BAND_MODE, "AUTO")
cg.add(
+5 -10
View File
@@ -724,8 +724,7 @@ async def _to_code_esp32(var: cg.MockObj, config: ConfigType) -> None:
cg.add(var.set_polling_interval(config[CONF_POLLING_INTERVAL]))
if _is_framework_spi_polling_mode_supported():
cg.add_define("USE_ETHERNET_SPI_POLLING_SUPPORT")
if CONF_RESET_PIN in config:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add_optional(var.set_reset_pin, config.get(CONF_RESET_PIN))
cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED]))
cg.add_define("USE_ETHERNET_SPI")
@@ -746,8 +745,7 @@ async def _to_code_esp32(var: cg.MockObj, config: ConfigType) -> None:
elif config[CONF_TYPE] in ("GENERIC", "YT8531"):
# RGMII data pins come from the IDF default config; set MDC/MDIO + PHY addr.
_add_mdio_setters(var, config)
if CONF_POWER_PIN in config:
cg.add(var.set_power_pin(config[CONF_POWER_PIN]))
cg.add_optional(var.set_power_pin, config.get(CONF_POWER_PIN))
_add_phy_registers(var, config)
else:
_add_mdio_setters(var, config)
@@ -755,8 +753,7 @@ async def _to_code_esp32(var: cg.MockObj, config: ConfigType) -> None:
cg.add(var.set_clk_mode(clk_mode))
if (clk_pin := config[CONF_CLK][CONF_PIN]) != DEFAULT_CLK_PIN:
cg.add(var.set_clk_pin(clk_pin))
if CONF_POWER_PIN in config:
cg.add(var.set_power_pin(config[CONF_POWER_PIN]))
cg.add_optional(var.set_power_pin, config.get(CONF_POWER_PIN))
_add_phy_registers(var, config)
# Register Ethernet with the esp32 sdkconfig reconciler. It disables the
@@ -782,10 +779,8 @@ async def _to_code_rp2040(var: cg.MockObj, config: ConfigType) -> None:
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
if CONF_INTERRUPT_PIN in config:
cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
if CONF_RESET_PIN in config:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add_optional(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
cg.add_optional(var.set_reset_pin, config.get(CONF_RESET_PIN))
cg.add_define("USE_ETHERNET_SPI")
cg.add_library(_RP2_SPI_LIBRARIES[config[CONF_TYPE]], None)

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