mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b501e48ebd | ||
|
|
0572454bed | ||
|
|
8e50491ef9 | ||
|
|
e3df568cd7 | ||
|
|
1a80e3faf4 | ||
|
|
0a46c81770 | ||
|
|
633cf73a9d | ||
|
|
565ee2a14b | ||
|
|
be0f3a32f9 | ||
|
|
c7967c2601 |
@@ -1,123 +0,0 @@
|
||||
---
|
||||
name: hardware-test
|
||||
description: Test an ESPHome change on real boards over USB. Use this whenever a task asks to test, verify, reproduce, bench, A/B or soak a pull request, a branch or a crash on hardware, or mentions flashing, serial logs, esphome upload, a captive portal or fallback AP test, or results to post on a PR. Covers finding the serial ports, building from the right checkout, flashing each chip family (including the LibreTiny kits that stock tools cannot flash), bounded log capture, proven test flows and how to report results.
|
||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# Hardware testing
|
||||
|
||||
The goal of a hardware test is evidence a reviewer can trust: which build ran on which board, what was
|
||||
driven, what the logs show, and the same flow on a `dev` build when a quirk could be pre-existing.
|
||||
`$SKILL` below is this directory (`.agents/skills/hardware-test`).
|
||||
|
||||
## Ground rules
|
||||
|
||||
1. **Serial ports cannot be shared.** One bounded capture per port, stopped before any flash on that
|
||||
port, never an open-ended `esphome logs`. Stop a capture with `pkill -f "[s]erial_capture.py <port> "`,
|
||||
naming the port so a capture on another board survives; the bracket keeps the pattern from matching
|
||||
the shell that runs it. Never run `esphome logs` in a
|
||||
`while true` loop: the loop respawns it and holds the port.
|
||||
2. **Credentials stay local.** Put a `secrets.yaml` next to the scratch configs and reference it with
|
||||
`!secret`. Never paste a config with credentials into a PR, never commit scratch configs or logs.
|
||||
3. **Build from the tree under test.** `python -m esphome` puts the current directory first on
|
||||
`sys.path`, so run it with the checkout or worktree as the working directory and pass the yaml by
|
||||
its absolute path. A venv `esphome` entry point may import an installed snapshot instead of the
|
||||
checkout. Confirm with `python -c 'import esphome; print(esphome.__file__)'`, and for an A/B grep
|
||||
the build's `src/` for the change before trusting any number.
|
||||
4. **PR build and baseline build get different `name:` values.** Builds share
|
||||
`.esphome/build/<name>`, so the same name means `esphome upload` can flash the wrong binary. Saved
|
||||
Wi-Fi credentials are keyed by the name too, so a test that must start unprovisioned gets a fresh
|
||||
name every run.
|
||||
5. **Flash and RAM deltas are measured against the PR's merge base** (`git merge-base origin/dev HEAD`
|
||||
checked out in a throwaway worktree), with the baseline commit named in the table. When a delta
|
||||
looks odd, diff symbol sizes (`nm -S -C --size-sort` from the matching toolchain) before quoting it.
|
||||
6. **Prove a quirk is pre-existing before blaming the PR:** run the identical flow on a `dev` build in
|
||||
the same session and say so in the results.
|
||||
7. **Pipe safely.** Set `PYTHONUNBUFFERED=1` on any `esphome run` or `esphome logs` whose output is
|
||||
piped, or lines land minutes late. Bound every long command with `timeout` (coreutils) or
|
||||
`perl -e 'alarm shift; exec @ARGV' <seconds> <command>` on macOS without it.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Find the boards:** `python $SKILL/scripts/ports.py` (any Python with pyserial, for example the
|
||||
ESPHome venv). Port names follow the USB hub port and change on every replug, so never reuse a name
|
||||
from an earlier session. CH340 adapters (D1 mini, BW15 kit, BK7238, CB3S) all look the same; tell
|
||||
them apart with a 10 s capture using `serial_capture.py --reset` (ESP8266 prints its ROM banner at
|
||||
74880 baud, LibreTiny boards print `[LT]` and ESPHome lines, BK7238 prints its bootloader version).
|
||||
`ports.py --wait 2e8a` blocks until a Pico re-enumerates.
|
||||
2. **Make a scratch directory** outside the checkout, with `secrets.yaml` and the templates you need.
|
||||
One yaml per board per variant, file name equal to `name:`.
|
||||
3. **Compile** in the right tree:
|
||||
`(cd <worktree> && PYTHONUNBUFFERED=1 timeout 1500 python -m esphome compile <abs yaml> > <name>-compile.log 2>&1)`.
|
||||
Keep the RAM and flash lines at the end. Two ESP-IDF or two LibreTiny builds at the same time race
|
||||
over tool caches; run them one at a time.
|
||||
4. **Flash** per chip family (table below). Stop the capture on that port first.
|
||||
5. **Capture** with `serial_capture.py PORT SECONDS OUT` in the background, then poll the file with
|
||||
`grep` in an `until ... sleep 2` loop with a cap. USB CDC boards (Pico, nRF52, native USB ESP32)
|
||||
drop the port on reboot; the script reopens it until its deadline.
|
||||
6. **Drive** the test: a second board running a client config, `curl`, an aioesphomeapi script that
|
||||
presses a template button and subscribes to logs, MQTT publishes, or `interval:` and `on_boot:`
|
||||
automations in the config itself.
|
||||
7. **Analyze** with `grep -a -c` counts over the capture and keep an `-events.txt` extract of the lines
|
||||
that matter. Decode crashes with the platform's `addr2line` (see `reference/boards.md`).
|
||||
8. **Report** on the PR as a table: build SHA, board and framework, what was driven, counts, result,
|
||||
plus the `dev` control row and the exact flow in a `<details>` block. No credentials, no port names,
|
||||
no IP addresses.
|
||||
|
||||
## Flashing by chip family
|
||||
|
||||
| Board | Flash | Log |
|
||||
|---|---|---|
|
||||
| ESP8266 D1 mini (CH340) | `python -m esphome upload <yaml> --device <port>` | `serial_capture.py` at 115200; `esphome logs` on some CH340s emits garbage after the RTS reset. A failed esptool write can wedge the adapter until the board is replugged. `esptool erase_flash` before a test that needs an unprovisioned device: the preference sector survives a reflash |
|
||||
| ESP32 devkit (CP2102 or CH340) | `esphome upload --device <port>`, ESP-IDF or Arduino | `serial_capture.py`. Default Wi-Fi light sleep delays API driven actions by seconds; judge timing from device logs. `power_save_mode: none` for OTA overlap tests |
|
||||
| ESP32-S3, C3, C6, H2 native USB | `esphome upload --device /dev/cu.usbmodem*` | `logger: hardware_uart: USB_SERIAL_JTAG`. The USB console can wedge when the app takes over the console (0 bytes, esptool `No serial data received`); replug, or watch through `esphome logs --device <name>.local` |
|
||||
| Pico W / Pico 2 W | `esphome upload --device <port>` (1200 baud touch); first flash ever via BOOTSEL and `--device /Volumes/RPI-RP2` or `RP2350` | port re-enumerates after upload: `ports.py --wait 2e8a` then capture |
|
||||
| BW15 kit (RTL8720CF, rtl87xx) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_rtl87xx_kit.py flash write -d <port> <build>/.pioenvs/<name>/*.uf2`. Plain `esphome upload` and plain ltchiptool time out because opening the port asserts DTR/RTS and holds the chip in reset | `serial_capture.py --released`, never `esphome logs` |
|
||||
| BK7238 / CB3S (bk72xx, RTS on CEN) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_bk72xx.py flash write -d <port> <uf2>` (about 75 s) | logs are on UART2 by default; `logger: hardware_uart: UART1` puts them on the flashing adapter. Opening the port resets the chip, so keep one capture open for the whole test |
|
||||
| LN882H | ltchiptool forces 1 Mbaud for writes; weak adapters need `LN882H_YM_BAUDRATE` patched to 115200 in the penv's `ln882htool.py`; short BOOT to GND while the tool connects | OTA after the first flash |
|
||||
| nRF52840 Dongle (PCA10059) | Nordic `nrfutil nrf5sdk-tools dfu usb-serial` on the bootloader port, or `dfu: true` in the config so the 1200 baud touch pin-resets into the bootloader; adafruit-nrfutil is refused | app has two CDC ports, ESPHome log on the second |
|
||||
| Adafruit-style nRF52 (UF2 bootloader) | `UF2BOOT` volume or adafruit-nrfutil; `dfu: true` works through GPREGRET | single CDC port |
|
||||
| Any reachable device | `esphome upload <yaml> --device <ip or name.local>`; `--file <bin>` for a prebuilt image | `esphome logs --device <ip>` bounded with `timeout` |
|
||||
|
||||
ESP-IDF OTA image: `.esphome/build/<name>/build/firmware.ota.bin`. LibreTiny OTA port is 8892.
|
||||
|
||||
## Scripts in `$SKILL/scripts`
|
||||
|
||||
- `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
|
||||
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.
|
||||
- `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),
|
||||
`RESULT FAIL` (exit 3: a crash, a reboot after the AP came up, or the device never joined the network),
|
||||
`RESULT INVALID`
|
||||
(exit 2: a precondition was missed, the stimulus was insufficient, or a capture did not last the run)
|
||||
or exits 1 when a build, flash or port step failed.
|
||||
|
||||
## Templates in `$SKILL/templates`
|
||||
|
||||
- `dut-fallback-ap.yaml` (package): a device that never finds its network, falls back to its AP and
|
||||
serves the captive portal. `example-dut.yaml` is the per-run file with the platform block.
|
||||
- `client-wifisave.yaml` (package): joins that AP, posts N bogus `/wifisave` requests, then the real
|
||||
credentials from `!secret`. `example-client.yaml` is the per-run file. `bogus_saves: "1"` gives a
|
||||
plain provisioning check.
|
||||
|
||||
## When something looks wrong
|
||||
|
||||
- A capture stopped early or a line is missing: check the timestamps before trusting it.
|
||||
- The client never joins the AP: another board may still run an AP with the same SSID, or the device
|
||||
booted with saved credentials because the name was reused. One AP device at a time on the bench.
|
||||
- Builds come out byte-identical between branches: the wrong tree was imported (rule 3).
|
||||
- Multicast from the host never arrives on some access points: test it device to device. Broadcast
|
||||
is lossy under Wi-Fi power save, unicast is not. Throttle UDP floods to a fixed packet rate.
|
||||
- `api:` reboots a board after 15 min without a client: `reboot_timeout: 0s` on `api:` and `mqtt:` for
|
||||
soak tests.
|
||||
|
||||
`reference/boards.md` has per-chip facts and crash decoding; `reference/recipes.md` has the proven
|
||||
flows. Add new findings there.
|
||||
@@ -1,138 +0,0 @@
|
||||
# Board facts
|
||||
|
||||
Ports are not listed on purpose: they follow the USB hub port and change on every replug. Run
|
||||
`scripts/ports.py`.
|
||||
|
||||
## ESP8266 (D1 mini, NodeMCU, CH340 or CP2102)
|
||||
|
||||
- Opening the port usually resets the board through DTR, but not always; `serial_capture.py --reset`
|
||||
pulses RTS to capture from boot. 74880 baud shows the ROM boot messages (`rst cause`, `boot mode`).
|
||||
- `esphome logs` on some CH340s emits thousands of duplicated garbage lines right after the RTS reset;
|
||||
a plain pyserial capture at 115200 is reliable.
|
||||
- A failed esptool write can wedge the adapter (`termios` error 22, or an open that hangs) until replug.
|
||||
- The preference sector survives a reflash: a provisioned API key or saved Wi-Fi credentials stay
|
||||
active. `esptool erase_flash` gives a clean device.
|
||||
- `Panic core_esp8266_main.cpp:191 __yield` with `ctx: sys` means a yield from lwIP context (a web
|
||||
handler, an lwIP callback). Decode stacks with
|
||||
`xtensa-lx106-elf-addr2line -e <build>/.pioenvs/<name>/firmware.elf -f -C -i <addr...>` from the
|
||||
toolchain under `~/.platformio/packages/toolchain-xtensa/bin/` (PlatformIO builds) or the ESPHome
|
||||
cache directory (native toolchain builds).
|
||||
- Constant data lives in RAM unless marked `PROGMEM`; with 80 KB of RAM a few hundred bytes are worth a
|
||||
row in the PR.
|
||||
- AsyncMqttClient drops inbound topics over 128 characters; topics of 255 or more wrap a counter and
|
||||
stall the link until the broker drops TCP.
|
||||
|
||||
## ESP32 classic devkit
|
||||
|
||||
- Takes ESP-IDF and Arduino. Default Wi-Fi light sleep delays API driven actions by up to 6 s; judge
|
||||
timings from device logs, not from the host. `power_save_mode: none` for OTA overlap tests (an IDF web
|
||||
upload of 739 KB took 107 s with power save and 45 s without).
|
||||
- Receives only a few percent of IPv6 multicast on some access points with power save on; a raw lwIP
|
||||
socket loses the same, so that is the AP or driver, not a PR.
|
||||
- Decode with `xtensa-esp32-elf-addr2line` from the IDF tools directory against
|
||||
`<build>/.pioenvs/<name>/firmware.elf` (PlatformIO) or `<build>/build/<name>.elf` (IDF native).
|
||||
- Clean reboot between runs with the capture stopped: `esptool --after hard_reset chip_id`.
|
||||
- Trigger a forced crash only after the app is marked valid (45 s after boot is plenty), never on an
|
||||
image still pending OTA rollback.
|
||||
- Signed OTA testing: build with `esp32.framework.advanced.signed_ota_verification.verification_keys`
|
||||
under a different name, sign with `python -m espsecure sign_data --version 2 --keyfile
|
||||
tests/components/esp32/dummy_signing_key.pem`, upload with `esphome upload --file`. The verifier
|
||||
refuses unsigned images afterwards, so restore with a signed plain build.
|
||||
|
||||
## ESP32-S3, C3, C6, H2 with native USB
|
||||
|
||||
- `/dev/cu.usbmodem*`, esptool works, `logger: hardware_uart: USB_SERIAL_JTAG`.
|
||||
- The USB console can die at the moment the app takes over the console during a reboot (log stops after
|
||||
`Disabling RNG early entropy source`, then 0 bytes, esptool reports `No serial data received`). Only a
|
||||
USB replug fixes it. Fallback: `esphome logs --device <name>.local` (API log stream, which drops lines
|
||||
while Wi-Fi is off).
|
||||
- The ESP32-P4 before silicon revision 3 is bricked by rev3-only firmware (boot loop at bootloader entry).
|
||||
|
||||
## RP2040 / RP2350 (Pico W, Pico 2 W)
|
||||
|
||||
- The CDC port re-enumerates after every reboot and after `esphome upload` (1200 baud touch). Re-detect
|
||||
with `ports.py --wait 2e8a` before capturing; `serial_capture.py` reopens on its own if the port
|
||||
vanishes mid-capture.
|
||||
- First flash ever: BOOTSEL, then `esphome upload --device /Volumes/RPI-RP2` (or `RP2350`).
|
||||
- arduino-pico creates the lwIP netif in `WiFi.begin()`, so any IGMP join during component setup fails;
|
||||
joins must wait for `network::is_connected()`.
|
||||
- Parallel Pico builds race on package extraction after a PlatformIO package wipe; one at a time.
|
||||
- An RTS pulse from pyserial does not reset the board; reboot with `App.safe_reboot()` or the touch.
|
||||
|
||||
## BW15 kit (RTL8720CF, LibreTiny rtl87xx)
|
||||
|
||||
- The kit's CH340 wires RTS to CEN and DTR to PA00 (boot strap). A plain pyserial open asserts both and
|
||||
holds the chip in reset, which is why `esphome upload`, plain `ltchiptool` and `esphome logs` all fail
|
||||
("Timeout while linking", or silence). `scripts/flash_rtl87xx_kit.py` sets DTR high and RTS released
|
||||
on every open, pulses RTS once so the ROM answers `ping`, and after the command drops DTR and pulses
|
||||
RTS to boot the app. Run it with the PlatformIO LibreTiny penv Python.
|
||||
- `flash write -d <port> <uf2>` takes no family argument; `flash read -d <port> -s 0 -l <len>
|
||||
realtek-ambz2 <out>` does.
|
||||
- Capture with `serial_capture.py --released`. The ESPHome log is on the flashing UART already.
|
||||
- The macOS CH340 driver only pushes DTR/RTS to the wire once a session has driven them, so the first
|
||||
plain open after a replug streams logs and the second is silent; always open with the lines released.
|
||||
- Driver quirks seen on `dev`: `WiFi.begin` can refuse the real network on the first try after bogus
|
||||
credentials (it connects after two cooldowns); right after connecting while the AP is still up the
|
||||
driver can bus-fault in `rtw_is_adapter_up` (`R0 = 0xdeadbeef`) and reboot; the AP restarts on every
|
||||
STA retry, so stations cannot stay joined; `WiFi.begin` fails synchronously for an absent network and
|
||||
lands in a cooldown at once; connecting blocks the loop 2.5 to 4.1 s.
|
||||
- Decode faults with `arm-none-eabi-addr2line -e <build>/.pioenvs/<name>/raw_firmware.elf -f -C -i <PC>`
|
||||
from `~/.platformio/packages/toolchain-gccarmnoneeabi/bin/` (`raw_firmware.elf`, not `firmware.elf`).
|
||||
Fault dumps print PC, LR and R0 to R3.
|
||||
- The AmebaZ2 SDK sets `LWIP_TCPIP_CORE_LOCKING 0`.
|
||||
|
||||
## BK7238, WB3S, CB3S (BK7231N) (LibreTiny bk72xx, CH340, RTS on CEN)
|
||||
|
||||
- Stock ltchiptool closes and reopens the port after its reset, and pyserial raises both lines on open,
|
||||
holding CEN low through the BootROM window. `scripts/flash_bk72xx.py` opens with both released and
|
||||
pulses the reset right before the link checks. A write takes about 75 s. Only a CEN or power reset
|
||||
reaches the downloader; no software reboot does.
|
||||
- ESPHome logs on UART2 by default; `logger: hardware_uart: UART1` puts them on the flashing adapter.
|
||||
- Opening the port resets the chip (bootloader banner on every open, even with DTR/RTS low), so keep one
|
||||
capture open for the whole test and never reopen mid-phase.
|
||||
- Callbacks run on the `async_tcp` task and log lines can lose a byte. Wi-Fi connect takes tens of ms;
|
||||
radio enable 160 to 278 ms, disable about 200 ms.
|
||||
|
||||
## BW16 kit (RTL8720DN, AmebaD)
|
||||
|
||||
- The kit's own USB-C (CH340) is on LOG_UART and flashes fine. Its two-transistor circuit resets while
|
||||
RTS alone is asserted and straps BURN while DTR alone is; both asserted does nothing. ROM entry is
|
||||
esptool's classic sequence (`dtr:false,rts:true` 100 ms, `dtr:true,rts:false` 400 ms, both false); the
|
||||
first pulse after a replug can be lost, retry up to three times.
|
||||
- Any reopen resets the chip, 460800 is flaky, 115200 is solid: one open per session, in-place baud
|
||||
changes only. The ROM and the loader NAK continuously while idle, so ACK scans must drain first.
|
||||
- The AmebaD core has no mDNS responder (`mdns: disabled: true`).
|
||||
|
||||
## LN882H
|
||||
|
||||
- ltchiptool forces 1 Mbaud for writes; adapters that cannot hold it fail with "Timeout while linking"
|
||||
or die mid-write. Patch `LN882H_YM_BAUDRATE` in the penv's `ltchiptool/soc/ln882h/util/ln882htool.py`
|
||||
to 115200 (lost on reprovision). Weak 3.3 V from the adapter: feed the module's 5 V pad instead. Short
|
||||
BOOT to GND while the tool connects. OTA afterwards.
|
||||
- `USE_WIFI_AP_EXCLUSIVE` (one radio mode at a time) exists only on LN882x; compile-check changes there.
|
||||
|
||||
## nRF52840
|
||||
|
||||
- Nordic PCA10059 dongle: the factory Open DFU bootloader enumerates as `1915:521F` on its own port
|
||||
name; the app as `2FE3:0100` with two CDC ports, ESPHome log on the second. Only Nordic
|
||||
`nrfutil nrf5sdk-tools` works (`pkg generate --hw-version 52 --sd-req 0x00 --application-version 1
|
||||
--application zephyr.hex app.zip`, then `dfu usb-serial -pkg app.zip -p <port>`); ESPHome downloads
|
||||
it on the first build. GPREGRET does not enter the bootloader; a pin reset through P0.19 does, which
|
||||
`dfu: true` wires up on this board. A pulsing red LED means bootloader; the side RESET button
|
||||
re-enters it.
|
||||
- Makerdiary MDK USB dongle: UF2 bootloader without SoftDevice, app at 0x1000, `UF2BOOT` volume,
|
||||
adafruit-nrfutil, GPREGRET 0x57 works. RGB LED active low (red P0.23, green P0.22, blue P0.24),
|
||||
button P0.18.
|
||||
|
||||
## ESP Thread Border Router board v1.2 (S3 host + H2 RCP)
|
||||
|
||||
- S3 host on USB JTAG serial, H2 RCP on its own USB. Spinel UART1 from the host: `rx_pin: GPIO17`,
|
||||
`tx_pin: GPIO18`, RCP reset `GPIO7` active low, 460800. The config needs `network: enable_ipv6: true`.
|
||||
- The shipped H2 firmware fails `CheckRadioCapabilities` with IDF 5.5; build `examples/openthread/ot_rcp`
|
||||
for esp32h2 with ESPHome's own IDF (`IDF_PATH` at the cached framework, its penv and tools,
|
||||
`IDF_PYTHON_CHECK_CONSTRAINTS=no`) and flash with esptool at 0x0, 0x8000 and 0x10000.
|
||||
- A crash-looping S3 pulses the H2 reset every boot, so the H2 cannot be flashed and the S3 will not
|
||||
enter its bootloader over USB. Hold S3 BOOT, tap S3 RST, flash the H2, release, then replug both.
|
||||
- `dns-sd -L <name> _meshcop._udp local.` shows the `omr` prefix; it disappears when routing is dead.
|
||||
Keep one capture per chip; never reset the H2 over USB mid-test unless that is the test (the host
|
||||
logs `Unexpected RCP reset: RESET_OTHER` and recovers).
|
||||
@@ -1,117 +0,0 @@
|
||||
# Proven test flows
|
||||
|
||||
Each flow names the pull requests it was used on, so their result tables show what to post.
|
||||
|
||||
## Captive portal `/wifisave` hammer (#20802, #20784)
|
||||
|
||||
Reproduces a `/wifisave` arriving during the STA cooldown, and proves a fix on every board.
|
||||
Device under test: `templates/example-dut.yaml` (bogus network, `ap:` with `ap_timeout: 15s`,
|
||||
`captive_portal:`). Client: `templates/example-client.yaml` on an ESP32 or a D1 mini; it joins the
|
||||
device's AP, posts 15 bogus saves 3 s apart, then the real credentials. With the portal up the cooldown
|
||||
is 30 s, so nearly every save lands inside one. Driver: `scripts/wifisave_hammer.sh`. Settle 40 s on
|
||||
ESP boards, 90 s on the BW15 (its first attempt on the real network fails, see boards.md).
|
||||
|
||||
Counts that matter: `Requested WiFi Settings Change` (saves received), `Exiting cooldown early`, crash
|
||||
markers, boots (`Log initialized`), and the client's `bogus save status 200` and `real save status 200`.
|
||||
A healthy run: every save answered 200, one cooldown exit per save (two on the BW15, whose retry enters
|
||||
the adapter restart), no panic, and the real credentials connect the device within about 10 s.
|
||||
|
||||
Fault injection for AP start failures: wrap `this->ap_setup_ = this->wifi_start_ap_(this->ap_)` in
|
||||
`setup_ap_config_()` with an `HWTEST_AP_FAIL_COUNT` build flag
|
||||
(`esphome: platformio_options: build_flags: -DHWTEST_AP_FAIL_COUNT=2`), raise the portal's "Captive
|
||||
portal started" from LOGV to LOGI, and after provisioning curl the device's STA address: connection
|
||||
refused proves the portal's server is gone.
|
||||
|
||||
## `reboot_timeout` with a fallback AP (#9958)
|
||||
|
||||
Only one board runs the AP at a time, or stations join the wrong one. AP probe: bogus network,
|
||||
`ap: {ssid, password, ap_timeout: 5s}`, `reboot_timeout: 60s`, `logger: level: VERBOSE` (ESP32 and
|
||||
ESP8266 log `AP client connected MAC=` at verbose), `on_boot: logger.log: BOOT`. Station probes:
|
||||
`reboot_timeout: 0s`, `interval: 150s` running `wifi.disable`, `delay: 20s`, `wifi.enable` (60 s cycles
|
||||
are too short to cover the due moment). Pass pattern: "Can't connect; rebooting" exactly 60 s after boot
|
||||
with no station joined, no reboot for minutes while one is, then a reboot within a second of the last
|
||||
disconnect. The BW15 restarts its AP on every STA retry and cannot be tested this way.
|
||||
|
||||
## MQTT (#20433, #17935, #20762, #20767, #20768)
|
||||
|
||||
A local mosquitto with `allow_anonymous true`, `log_type all` and `log_dest file`: the broker log gives
|
||||
each subscription's QoS, the device address (`New client connected from <ip> as <name>-<mac>`) and exact
|
||||
subscribe counts. Kill and restart mosquitto to exercise `on_disconnect` (reason 0) and resubscription.
|
||||
Keep `reboot_timeout: 0s` on `api:` and `mqtt:`. The ESP-IDF outbox swings free heap about 2 kB right
|
||||
after bulk subscribe and unsubscribe; read the heap seconds later over several rounds before calling a
|
||||
leak. Spawned subscriptions need about 2.5 s to reach the broker before the next publish.
|
||||
`esphome logs --device MQTT` only uses the MQTT transport when the config has no `api:`. Driver pattern: a
|
||||
paho script publishes and polls the serial capture file.
|
||||
|
||||
## UDP, multicast, sockets (#20556 to #20559, #20585, #20590)
|
||||
|
||||
A listener on port 9 needs no privileges on macOS. Test multicast device to device; host to device
|
||||
multicast never arrives on some access points, and `239.255.x.x` groups are unreliable even to a stock
|
||||
listener. Broadcast to boards is lossy under power save, unicast is not. Flood at a fixed packet rate
|
||||
(the host hits `ENOBUFS` unthrottled); 1500 packets per second survived on every board. A forced Wi-Fi
|
||||
reconnect on the Pico keeps IGMP membership. Set a long loop interval from `wifi.on_connect`
|
||||
(`App.set_loop_interval(20000)`), not `on_boot`, or Wi-Fi connect crawls. The `logger.log` action
|
||||
rejects `%.*s`; use a lambda.
|
||||
|
||||
## OTA overlap and lock (#20588), OTA timeouts (#19041), deflate (#19037)
|
||||
|
||||
All overlap orders (a web upload during a native OTA and the reverse) must be refused; the lock release
|
||||
after an interrupted web upload waits about 14 s (ESP8266) or 7 s (ESP-IDF) for disconnect detection. A
|
||||
refused web client on `web_server_idf` never gets the busy reply; the connection drops at reboot.
|
||||
`pkill -f "a\|b"` does not alternate; use two pkills.
|
||||
|
||||
## DHCP lease cache (#17767)
|
||||
|
||||
Cycle 20 s awake, `deep_sleep.enter 30s`, wake, 20 s, `App.safe_reboot()`, logging `millis()` at the
|
||||
`wifi.connected` condition. An RTS hardware reset must fall back to DHCP; a software reboot and a deep
|
||||
sleep wake must apply the cache. `esphome run --no-logs` silently skipped the upload on one board; use
|
||||
`upload` then `logs`.
|
||||
|
||||
## HDMI-CEC two-board wire (#9189)
|
||||
|
||||
Two boards on one wire: promiscuous mode, a 0x8F request answered with 0x90 0x00, a `destination:`
|
||||
filter on the reply handler, template buttons for each target plus lambdas calling
|
||||
`send(source, dest, span)` as another initiator. Healthy numbers: device-side turnaround 15 to 22 ms,
|
||||
round trip 144 to 156 ms. Press buttons over the API; judge timings from device logs (ESP32 light sleep
|
||||
lags the press).
|
||||
|
||||
## LD2450 polygon zones (#20154)
|
||||
|
||||
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.
|
||||
|
||||
## Scheduler and loop wake A/B (#20356)
|
||||
|
||||
An external component whose template button arms an SDK `os_timer` (50 ms, one shot) on the ESP8266;
|
||||
the callback runs in SYS context, records `millis()` and calls `set_timeout` or `defer`; the timeout
|
||||
logs the elapsed time and loop count delta. Drive with an aioesphomeapi script (`subscribe_logs` plus
|
||||
`button_command`). Build the same yaml twice with different names, one per tree.
|
||||
|
||||
## Noise handshake and OTA encryption on the ESP8266 (#19062, #19233)
|
||||
|
||||
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.
|
||||
|
||||
## ESP-NOW on the ESP8266 (#14407)
|
||||
|
||||
The NONOS SDK only delivers broadcast ESP-NOW frames while the station is associated or a soft-AP is
|
||||
up; a hidden, open, client-less soft-AP (60 s beacon, DHCP stopped) makes broadcasts arrive. In AP-only
|
||||
mode the device transmits from the soft-AP MAC.
|
||||
|
||||
## Wi-Fi Easy Connect (DPP) enrollee (#17934)
|
||||
|
||||
Only an Android phone configures: Settings, network details, Add device, Share Wi-Fi (the Wi-Fi list's
|
||||
QR scanner makes the phone an enrollee). Render the URI with the `qrcode` module and open the PNG.
|
||||
`esp32: framework: log_level: DEBUG` plus `sdkconfig_options: CONFIG_WPA_DEBUG_PRINT: y` prints the
|
||||
supplicant state machine.
|
||||
|
||||
## Thread border router (#18104)
|
||||
|
||||
See boards.md for the ESP Thread BR board. Check that the H2 answers Spinel before blaming a PR.
|
||||
|
||||
## nRF52840 dongle boards (#19009, #20814)
|
||||
|
||||
Test config: the two LEDs as inverted GPIO outputs, the button as an inverted pull-up binary sensor, a
|
||||
10 s alive interval. To retest the nrfutil download path, move the cached `sdk-nrf/nrfutil` aside first.
|
||||
@@ -1,51 +0,0 @@
|
||||
"""ltchiptool for boards whose RTS drives CEN: reset and link on one open port.
|
||||
|
||||
Stock ltchiptool closes and reopens the port after its DTR/RTS reset, and
|
||||
pyserial raises both lines on open, holding CEN low through the BootROM's
|
||||
listen window. Open with both lines released and pulse the reset right
|
||||
before the LinkChecks instead.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import bk7231tools.serial.main as bk_main
|
||||
from ltchiptool.__main__ import cli
|
||||
from ltchiptool.soc.bk72xx.flash import BK72XXFlash
|
||||
from serial import Serial
|
||||
|
||||
|
||||
class ReleasedSerial(Serial):
|
||||
def __init__(self, port=None, **kwargs):
|
||||
super().__init__(None, **kwargs)
|
||||
self.dtr = False
|
||||
self.rts = False
|
||||
self.port = port
|
||||
self.open()
|
||||
|
||||
|
||||
bk_main.Serial = ReleasedSerial
|
||||
|
||||
_connect = BK72XXFlash.flash_connect
|
||||
|
||||
|
||||
def flash_hw_reset(self) -> None:
|
||||
self._reset_pending = True
|
||||
|
||||
|
||||
def flash_connect(self) -> None:
|
||||
pending = getattr(self, "_reset_pending", False)
|
||||
if self.bk and self.conn.linked and not pending:
|
||||
return
|
||||
self.flash_build_protocol()
|
||||
if pending:
|
||||
self._reset_pending = False
|
||||
self.bk.hw_reset()
|
||||
self.conn.linked = False # the chip rebooted, so link again below
|
||||
_connect(self)
|
||||
|
||||
|
||||
BK72XXFlash.flash_hw_reset = flash_hw_reset
|
||||
BK72XXFlash.flash_connect = flash_connect
|
||||
|
||||
sys.argv = ["ltchiptool", *sys.argv[1:]]
|
||||
cli()
|
||||
@@ -1,87 +0,0 @@
|
||||
"""ltchiptool for the BW15 kit whose CH340 drives CEN (RTS) and PA00 (DTR).
|
||||
|
||||
A plain open asserts both lines and holds the chip in reset. Here every open keeps DTR high and RTS
|
||||
released; the first open pulses RTS so the ROM comes up; after the command DTR low plus an RTS pulse
|
||||
boots the app. The exit status is ltchiptool's; 2 means the flash succeeded but the app was not
|
||||
booted (boot pulse failed, or no -d PORT to send it to), so the board sits in the ROM until reset.
|
||||
|
||||
Usage: python flash_rtl87xx_kit.py flash write -d <port> firmware.uf2
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
|
||||
_first = [True]
|
||||
_orig_open = serial.Serial.open
|
||||
|
||||
|
||||
def _open(self):
|
||||
self.dtr = True # PA00 high: ROM download mode on the next reset
|
||||
self.rts = False # CEN released
|
||||
_orig_open(self)
|
||||
if _first[0]:
|
||||
_first[0] = False
|
||||
self.rts = True
|
||||
time.sleep(0.1)
|
||||
self.rts = False
|
||||
time.sleep(0.3) # reset into the ROM
|
||||
self.reset_input_buffer()
|
||||
|
||||
|
||||
serial.Serial.open = _open
|
||||
|
||||
from ltchiptool.__main__ import cli_entrypoint # noqa: E402
|
||||
|
||||
|
||||
def port_argument(argv: list[str]) -> str | None:
|
||||
for i, a in enumerate(argv):
|
||||
if a in ("-d", "--device") and i + 1 < len(argv):
|
||||
return argv[i + 1]
|
||||
if a.startswith(("-d=", "--device=")):
|
||||
return a.split("=", 1)[1]
|
||||
if a.startswith("-d") and len(a) > 2:
|
||||
return a[2:]
|
||||
return None
|
||||
|
||||
|
||||
def boot_app(port: str) -> None:
|
||||
s = serial.Serial(port=None)
|
||||
s.port = port
|
||||
s.dtr = False
|
||||
s.rts = False
|
||||
_orig_open(s)
|
||||
s.rts = True
|
||||
time.sleep(0.1)
|
||||
s.rts = False
|
||||
s.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rc = 1
|
||||
try:
|
||||
result = cli_entrypoint.main(args=sys.argv[1:], standalone_mode=False)
|
||||
rc = int(result) if isinstance(result, int) else 0
|
||||
finally:
|
||||
port = port_argument(sys.argv)
|
||||
if not port:
|
||||
print(
|
||||
"[flash_rtl87xx_kit] no -d PORT given, the application was not booted",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if rc == 0:
|
||||
rc = 2
|
||||
else:
|
||||
try:
|
||||
boot_app(port)
|
||||
print("[flash_rtl87xx_kit] application booted")
|
||||
except (serial.SerialException, OSError) as err:
|
||||
print(f"[flash_rtl87xx_kit] boot pulse failed: {err}", file=sys.stderr)
|
||||
if rc == 0:
|
||||
rc = 2 # flashed, but the board is still in the ROM: press reset
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,123 +0,0 @@
|
||||
"""List the boards on USB, or wait for one to enumerate.
|
||||
|
||||
ports.py table: port, vid:pid, product, serial number, guess
|
||||
ports.py --wait 2e8a print the first port with VID 0x2e8a once it appears (default 60 s)
|
||||
ports.py --wait 1a86:7523 --timeout 30
|
||||
ports.py --product "USB Serial" first port whose USB product string matches (case-insensitive)
|
||||
ports.py --wait 2e8a --serial SN the port of one specific board, whatever name it comes back under
|
||||
ports.py --serial-of PORT print the USB serial number behind a port name
|
||||
|
||||
Run with a Python that has pyserial (any ESPHome venv). Port names follow the hub port on macOS, so
|
||||
re-run this after every replug instead of trusting a name from an earlier session.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from serial.tools import list_ports
|
||||
|
||||
GUESS = [
|
||||
(
|
||||
(0x1A86, 0x7523),
|
||||
"CH340: D1 mini, BW15 kit, BK7238/CB3S or LN882H adapter; identify by the boot banner",
|
||||
),
|
||||
((0x1A86, 0x55D3), "CH343: ESP32-P4 devkit console"),
|
||||
((0x1A86, 0x55D4), "CH343: ESP32 devkit"),
|
||||
((0x1A86, None), "WCH CH34x USB serial bridge"),
|
||||
((0x10C4, 0xEA60), "CP2102: classic ESP32 devkit (esp32dev)"),
|
||||
(
|
||||
(0x303A, 0x1001),
|
||||
"Espressif USB JTAG serial: S3/C3/C6/H2 native USB (logger hardware_uart USB_SERIAL_JTAG)",
|
||||
),
|
||||
(
|
||||
(0x2E8A, None),
|
||||
"Raspberry Pi Pico W / Pico 2 W CDC; the port vanishes and comes back on every reboot",
|
||||
),
|
||||
((0x1915, 0x521F), "nRF52840 Dongle (PCA10059) in the Open DFU bootloader"),
|
||||
(
|
||||
(0x2FE3, 0x0100),
|
||||
"nRF52840 Dongle running a Zephyr app; two CDC ports, ESPHome log on the second",
|
||||
),
|
||||
((0x1915, 0xCAFE), "Makerdiary nRF52840 MDK dongle running a Zephyr app"),
|
||||
((0x239A, None), "Adafruit style nRF52 board (UF2 bootloader family)"),
|
||||
]
|
||||
|
||||
|
||||
def guess(vid: int, pid: int) -> str:
|
||||
for (gv, gp), text in GUESS:
|
||||
if vid == gv and (gp is None or pid == gp):
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
def parse_id(text: str) -> tuple[int, int | None]:
|
||||
vid, _, pid = text.partition(":")
|
||||
return int(vid, 16), (int(pid, 16) if pid else None)
|
||||
|
||||
|
||||
def matches(
|
||||
port, vid: int | None, pid: int | None, product: str | None, serial: str | None
|
||||
) -> bool:
|
||||
if vid is not None and (port.vid != vid or (pid is not None and port.pid != pid)):
|
||||
return False
|
||||
if product and (port.product or "").lower() != product.lower():
|
||||
return False
|
||||
return not serial or (port.serial_number or "") == serial
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
ap.add_argument(
|
||||
"--wait",
|
||||
metavar="VID[:PID]",
|
||||
help="block until a matching port appears, print it",
|
||||
)
|
||||
ap.add_argument("--product", help="USB product string to match")
|
||||
ap.add_argument("--serial", help="USB serial number to match")
|
||||
ap.add_argument(
|
||||
"--serial-of", metavar="PORT", help="print the USB serial number of PORT"
|
||||
)
|
||||
ap.add_argument("--timeout", type=float, default=60)
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.serial_of:
|
||||
for p in list_ports.comports():
|
||||
if p.device == args.serial_of and p.serial_number:
|
||||
print(p.serial_number)
|
||||
return 0
|
||||
print(f"no USB serial number for {args.serial_of}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
vid = pid = None
|
||||
if args.wait:
|
||||
vid, pid = parse_id(args.wait)
|
||||
|
||||
if args.wait or args.product or args.serial:
|
||||
deadline = time.time() + args.timeout
|
||||
while True:
|
||||
for p in sorted(list_ports.comports(), key=lambda p: p.device):
|
||||
if p.vid and matches(p, vid, pid, args.product, args.serial):
|
||||
print(p.device)
|
||||
return 0
|
||||
if time.time() >= deadline:
|
||||
print("no matching port", file=sys.stderr)
|
||||
return 1
|
||||
time.sleep(1)
|
||||
|
||||
rows = [p for p in list_ports.comports() if p.vid]
|
||||
if not rows:
|
||||
print("no USB serial ports")
|
||||
return 1
|
||||
for p in sorted(rows, key=lambda p: p.device):
|
||||
print(
|
||||
f"{p.device:32} {p.vid:04x}:{p.pid:04x} {p.product or '?':34} "
|
||||
f"{p.serial_number or '':18} {guess(p.vid, p.pid)}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,157 +0,0 @@
|
||||
"""Bounded, timestamped serial capture that always exits and closes the port.
|
||||
|
||||
serial_capture.py PORT SECONDS OUTFILE [--baud 115200] [--released] [--reset] [--append]
|
||||
|
||||
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
|
||||
(DTR) and reset (RTS), so a plain open holds the chip in reset.
|
||||
--reset pulse reset after opening (DTR low, RTS high 100 ms, RTS low) to capture a fresh boot.
|
||||
--baud 115200 by default; 74880 shows the ESP8266 ROM boot messages.
|
||||
--append keep what OUTFILE already holds; by default it is truncated so counts cover this run only.
|
||||
|
||||
Exit status 1 when nothing was received (wrong port name, or a port another capture still holds).
|
||||
Text ESPHome marks as secret (LOG_SECRET, the Wi-Fi password) is written as <secret>.
|
||||
Lines are written as [HH:MM:SS]text with ANSI colour stripped and flushed one by one, so a shell
|
||||
loop can grep the file while the capture runs. If the port is missing or disappears (USB CDC boards
|
||||
re-enumerate on reboot) the script retries until the deadline, and follows the board by its USB serial
|
||||
number when it comes back under another device path.
|
||||
|
||||
Never run an open-ended serial session: every capture has a deadline, and a flash on the same port
|
||||
needs the capture stopped first (pkill -f "[s]erial_capture.py" does not match the killing shell).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
||||
ANSI = re.compile(rb"\x1b\[[0-9;]*[A-Za-z]")
|
||||
# ESPHome's LOG_SECRET span (a Wi-Fi password); a span cut off before its end marker is secret to the end
|
||||
SECRET = re.compile(rb"\x1b\[8m.*?(?:\x1b\[28m|$)", re.DOTALL)
|
||||
PORT_ERRORS = (serial.SerialException, OSError)
|
||||
|
||||
|
||||
def identify(port: str) -> tuple[str | None, int]:
|
||||
"""USB serial number of the port and its position among that device's ports (a dongle has two)."""
|
||||
ports = sorted(list_ports.comports(), key=lambda p: p.device)
|
||||
serial_number = next((p.serial_number for p in ports if p.device == port), None)
|
||||
siblings = [
|
||||
p.device for p in ports if serial_number and p.serial_number == serial_number
|
||||
]
|
||||
return serial_number, siblings.index(port) if port in siblings else 0
|
||||
|
||||
|
||||
def current_path(port: str, serial_number: str | None, index: int) -> str:
|
||||
"""The port's current device path: a USB CDC board can come back under a new name after a reboot."""
|
||||
if serial_number:
|
||||
ports = sorted(list_ports.comports(), key=lambda p: p.device)
|
||||
siblings = [p.device for p in ports if p.serial_number == serial_number]
|
||||
if index < len(siblings):
|
||||
return siblings[index]
|
||||
return port
|
||||
|
||||
|
||||
def open_port(args: argparse.Namespace, path: str) -> serial.Serial:
|
||||
s = serial.Serial(port=None, baudrate=args.baud, timeout=0.2)
|
||||
s.port = path
|
||||
if args.released:
|
||||
s.dtr = False
|
||||
s.rts = False
|
||||
s.open()
|
||||
if args.reset:
|
||||
try:
|
||||
s.dtr = False
|
||||
s.rts = True
|
||||
time.sleep(0.1)
|
||||
s.rts = False
|
||||
s.reset_input_buffer()
|
||||
except PORT_ERRORS:
|
||||
s.close()
|
||||
raise
|
||||
return s
|
||||
|
||||
|
||||
def write_line(out, tag: bytes, data: bytes) -> None:
|
||||
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 capture(args: argparse.Namespace, out) -> int:
|
||||
"""Returns the number of bytes received."""
|
||||
deadline = time.time() + args.seconds
|
||||
buf = b""
|
||||
s = None
|
||||
received = 0
|
||||
serial_number, index = None, 0
|
||||
while time.time() < deadline:
|
||||
if s is None:
|
||||
path = current_path(args.port, serial_number, index)
|
||||
try:
|
||||
s = open_port(args, path)
|
||||
args.reset = False # only reset on the first open
|
||||
if serial_number is None:
|
||||
serial_number, index = identify(path)
|
||||
if path != args.port:
|
||||
note(out, f"reopened as {path}")
|
||||
except PORT_ERRORS as err:
|
||||
note(out, f"open failed: {err}")
|
||||
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)
|
||||
buf = b""
|
||||
note(out, f"port lost: {err}")
|
||||
with contextlib.suppress(*PORT_ERRORS):
|
||||
s.close()
|
||||
s = None
|
||||
time.sleep(1)
|
||||
continue
|
||||
if not data:
|
||||
continue
|
||||
received += len(data)
|
||||
buf += data
|
||||
while b"\n" in buf:
|
||||
line, buf = buf.split(b"\n", 1)
|
||||
write_line(out, b"", line.rstrip(b"\r"))
|
||||
if buf:
|
||||
write_line(out, b"[partial]", buf)
|
||||
if s is not None:
|
||||
s.close()
|
||||
return received
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
ap.add_argument("port")
|
||||
ap.add_argument("seconds", type=float)
|
||||
ap.add_argument("outfile")
|
||||
ap.add_argument("--baud", type=int, default=115200)
|
||||
ap.add_argument("--released", action="store_true")
|
||||
ap.add_argument("--reset", action="store_true")
|
||||
ap.add_argument("--append", action="store_true")
|
||||
args = ap.parse_args()
|
||||
with pathlib.Path(args.outfile).open("ab" if args.append else "wb") as out:
|
||||
received = capture(args, out)
|
||||
if received == 0:
|
||||
print(f"[capture] nothing received from {args.port}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/bin/bash
|
||||
# wifisave_hammer.sh: the device under test (DUT) falls back to its AP; a client board joins it and posts
|
||||
# /wifisave repeatedly, then the real credentials. Compiles, flashes, captures both serial logs and
|
||||
# prints counts. Configs come from templates/example-dut.yaml and templates/example-client.yaml.
|
||||
#
|
||||
# Usage: wifisave_hammer.sh DUT_KIND DUT_PORT DUT_YAML DUT_TREE CLIENT_PORT CLIENT_YAML CLIENT_TREE [SETTLE_S]
|
||||
# DUT_KIND esp (esphome upload), pico (upload, then re-find the CDC port by USB serial number),
|
||||
# bw15 (flash_rtl87xx_kit.py) or bk (flash_bk72xx.py)
|
||||
# *_YAML absolute path; the file name without .yaml must equal the config's `name`
|
||||
# *_TREE checkout or worktree whose esphome code must build that yaml (cwd for python -m esphome)
|
||||
# SETTLE_S seconds to keep capturing after the client reports the real save (default 40; BW15 wants 90)
|
||||
# Env: PY (python with esphome; default DUT_TREE/venv/bin/python, else python3 on PATH; an explicit PY must exist), LTPY
|
||||
# (LibreTiny penv python with ltchiptool; default ~/.platformio/penv/.libretiny/bin/python)
|
||||
# Output next to each yaml: NAME-compile.log, NAME-upload.log, NAME-serial.log, NAME-capture.err, and DUT NAME-events.txt
|
||||
# (addresses redacted so the extract can go on a PR; the serial logs themselves stay local).
|
||||
# Exit status: 0 RESULT PASS (no crash marker or reboot after the AP came up, the device joined the network,
|
||||
# and real stimulus: saves answered and a cooldown exit); 3 RESULT FAIL (the counts show
|
||||
# a failure); 2 RESULT INVALID (a precondition or the stimulus was missed, or a capture did not last the run);
|
||||
# 1 when a build, flash or port step fails (nothing is reported).
|
||||
set -u
|
||||
SKILL=$(cd "$(dirname "$0")/.." && pwd)
|
||||
KIND=$1; DPORT=$2; DYAML=$3; DTREE=$4; CPORT=$5; CYAML=$6; CTREE=$7; SETTLE=${8:-40}
|
||||
if [ -n "${PY:-}" ]; then [ -x "$PY" ] || { echo "[hammer] FAILED: PY=$PY is not executable"; exit 1; }
|
||||
else PY=$DTREE/venv/bin/python; [ -x "$PY" ] || PY=$(command -v python3) || { echo "[hammer] FAILED: no python found, set PY"; exit 1; }; fi
|
||||
LTPY=${LTPY:-$HOME/.platformio/penv/.libretiny/bin/python}
|
||||
DDIR=$(dirname "$DYAML"); DNAME=$(basename "$DYAML" .yaml)
|
||||
CDIR=$(dirname "$CYAML"); CNAME=$(basename "$CYAML" .yaml)
|
||||
DLOG=$DDIR/$DNAME-serial.log; CLOG=$CDIR/$CNAME-serial.log
|
||||
INVALID=""
|
||||
AP_WAIT=120; UPLOAD_WAIT=300; SAVE_WAIT=240
|
||||
# Crash signatures (extended regex), not bare words: "aborting connection attempt" is a normal Wi-Fi retry line
|
||||
CRASH_RE='Panic |abort\(\)|Fatal exception|Exception \(|Guru Meditation|HardFault|Bus Fault|Backtrace:|Soft WDT reset|wdt reset|Brownout|rst cause:[24]'
|
||||
# Boot banners and port losses are counted apart: a Pico reboot shows as a port loss (and maybe a banner too),
|
||||
# while on a UART board a port loss comes from the adapter or the hub, not the chip
|
||||
BANNER_RE='Log initialized'; LOSS_RE='\[capture\] port lost'
|
||||
DUT_CAPTURE=$((AP_WAIT + UPLOAD_WAIT + SAVE_WAIT + SETTLE + 60)); CLIENT_CAPTURE=$((SAVE_WAIT + SETTLE + 60))
|
||||
|
||||
bound() { local t=$1; shift; if command -v timeout >/dev/null; then timeout "$t" "$@"; else perl -e 'alarm shift; exec @ARGV' "$t" "$@"; fi; }
|
||||
count() { LC_ALL=C grep -a -c "$1" "$2"; }
|
||||
wait_for() { # wait_for FILE PATTERN SECONDS -> prints elapsed, status 1 on timeout
|
||||
local n=0; until LC_ALL=C grep -a -q "$2" "$1"; do sleep 2; n=$((n+2)); [ $n -ge "$3" ] && { echo "$n"; return 1; }; done; echo "$n"; }
|
||||
# Only this run's captures: another capture on the host (a soak on a third board) must survive
|
||||
stop_tails() { pkill -f "[s]erial_capture.py $DPORT " 2>/dev/null; pkill -f "[s]erial_capture.py $CPORT " 2>/dev/null; sleep 1; }
|
||||
fail() { echo "[hammer] FAILED: $1"; stop_tails; exit 1; }
|
||||
port_never_opened() { # an open failure and no line that is not a capture note: the port, not the firmware
|
||||
LC_ALL=C grep -a -q '\[capture\] open failed' "$1" && ! LC_ALL=C grep -a -q -v '^\[[0-9:]*\]\[capture\]' "$1"; }
|
||||
compile() { # compile YAML TREE
|
||||
(cd "$2" && PYTHONUNBUFFERED=1 bound 1500 "$PY" -m esphome compile "$1" > "${1%.yaml}-compile.log" 2>&1); local rc=$?
|
||||
echo "[hammer] compile $(basename "$1"): rc=$rc"; return $rc; }
|
||||
esp_upload() { # esp_upload YAML PORT TREE
|
||||
(cd "$3" && bound $UPLOAD_WAIT "$PY" -m esphome upload "$1" --device "$2" > "${1%.yaml}-upload.log" 2>&1)
|
||||
local ok; ok=$(count 'Successfully uploaded' "${1%.yaml}-upload.log"); echo "[hammer] upload $(basename "$1"): ok=$ok"; [ "$ok" -ge 1 ]; }
|
||||
|
||||
stop_tails
|
||||
compile "$DYAML" "$DTREE" || fail "DUT build failed, see $DDIR/$DNAME-compile.log"
|
||||
compile "$CYAML" "$CTREE" || fail "client build failed, see $CDIR/$CNAME-compile.log"
|
||||
|
||||
TAILOPT=""
|
||||
case $KIND in
|
||||
esp) esp_upload "$DYAML" "$DPORT" "$DTREE" || fail "DUT upload failed" ;;
|
||||
pico)
|
||||
SN=$("$PY" "$SKILL/scripts/ports.py" --serial-of "$DPORT") || fail "no USB serial number for $DPORT"
|
||||
esp_upload "$DYAML" "$DPORT" "$DTREE" || fail "DUT upload failed"
|
||||
# The Pico drops its CDC port on reboot and may already be back: take whatever path this board has now;
|
||||
# the capture follows the serial number if the path changes again
|
||||
DPORT=$("$PY" "$SKILL/scripts/ports.py" --wait 2e8a --serial "$SN" --timeout 60) || fail "pico did not re-enumerate"
|
||||
echo "[hammer] pico port after upload: $DPORT"; sleep 2 ;;
|
||||
bw15|bk)
|
||||
UF2=$(ls "$DDIR/.esphome/build/$DNAME/.pioenvs/$DNAME/"*.uf2 2>/dev/null | head -1)
|
||||
[ -n "$UF2" ] || fail "no uf2 under $DDIR/.esphome/build/$DNAME"
|
||||
echo "[hammer] uf2: ${UF2##*/}"
|
||||
W=$SKILL/scripts/flash_rtl87xx_kit.py; [ "$KIND" = bk ] && W=$SKILL/scripts/flash_bk72xx.py
|
||||
(cd "$DDIR" && bound 400 "$LTPY" "$W" flash write -d "$DPORT" "$UF2" > "$DDIR/$DNAME-upload.log" 2>&1); rc=$?
|
||||
echo "[hammer] $KIND flash: rc=$rc"; [ $rc -eq 0 ] || fail "DUT flash failed, see $DDIR/$DNAME-upload.log"
|
||||
TAILOPT="--released" ;;
|
||||
*) echo "unknown DUT_KIND $KIND"; exit 1 ;;
|
||||
esac
|
||||
|
||||
: > "$DLOG"; ("$PY" "$SKILL/scripts/serial_capture.py" "$DPORT" $DUT_CAPTURE "$DLOG" $TAILOPT 2> "${DLOG%-serial.log}-capture.err" &)
|
||||
if ! n=$(wait_for "$DLOG" "Captive portal\|Starting fallback AP" $AP_WAIT); then
|
||||
[ -s "$DLOG" ] || fail "the DUT capture produced nothing (the capture itself failed), see ${DLOG%-serial.log}-capture.err"
|
||||
port_never_opened "$DLOG" && fail "could not open $DPORT (held by another process, or the wrong port), see $DLOG"
|
||||
fail "DUT never raised its AP within ${n}s, see $DLOG"
|
||||
fi
|
||||
echo "[hammer] DUT fallback AP after ${n}s"; sleep 2
|
||||
# reboots are counted from here; the first banner may predate the capture
|
||||
BANNER_BASE=$(count "$BANNER_RE" "$DLOG"); LOSS_BASE=$(count "$LOSS_RE" "$DLOG")
|
||||
|
||||
esp_upload "$CYAML" "$CPORT" "$CTREE" || fail "client upload failed"
|
||||
: > "$CLOG"; ("$PY" "$SKILL/scripts/serial_capture.py" "$CPORT" $CLIENT_CAPTURE "$CLOG" 2> "${CLOG%-serial.log}-capture.err" &)
|
||||
if n=$(wait_for "$CLOG" "real save status\|posting the real" $SAVE_WAIT); then
|
||||
echo "[hammer] client posted the real credentials after ${n}s; settling ${SETTLE}s"; sleep "$SETTLE"
|
||||
else
|
||||
[ -s "$CLOG" ] || fail "the client capture produced nothing (the capture itself failed), see ${CLOG%-serial.log}-capture.err"
|
||||
port_never_opened "$CLOG" && fail "could not open $CPORT (held by another process, or the wrong port), see $CLOG"
|
||||
INVALID="client never reached the real save within ${n}s"; echo "[hammer] WARNING: $INVALID"
|
||||
fi
|
||||
|
||||
# A verdict needs complete evidence: both captures must have lasted the run and logged no error of their own
|
||||
for spec in "$DPORT:${DLOG%-serial.log}-capture.err" "$CPORT:${CLOG%-serial.log}-capture.err"; do
|
||||
port=${spec%%:*}; err=${spec#*:}
|
||||
pgrep -f "[s]erial_capture.py $port " >/dev/null || INVALID="${INVALID:+$INVALID; }the capture on $port ended before the run did, see $err"
|
||||
[ -s "$err" ] && INVALID="${INVALID:+$INVALID; }the capture on $port reported an error, see $err"
|
||||
done
|
||||
CRASHES=$(LC_ALL=C grep -a -c -E "$CRASH_RE" "$DLOG")
|
||||
BANNERS=$(( $(count "$BANNER_RE" "$DLOG") - BANNER_BASE )); LOSSES=$(( $(count "$LOSS_RE" "$DLOG") - LOSS_BASE ))
|
||||
if [ "$KIND" = pico ]; then
|
||||
REBOOTS=$BANNERS; [ "$LOSSES" -gt "$REBOOTS" ] && REBOOTS=$LOSSES # one reboot can show as both, so take the larger
|
||||
else
|
||||
REBOOTS=$BANNERS; [ "$LOSSES" -gt 0 ] && INVALID="${INVALID:+$INVALID; }the DUT port dropped $LOSSES time(s) mid-run (adapter or hub, not the chip)"
|
||||
fi
|
||||
STA=$(LC_ALL=C grep -a 'IP Address:' "$DLOG" | grep -v -c '192\.168\.4\.'); REAL=$(LC_ALL=C grep -a -o 'real save status [-0-9]*' "$CLOG" | tail -1 | awk '{print $4}')
|
||||
SAVES=$(count 'Requested WiFi Settings Change' "$DLOG"); EXITS=$(count 'cooldown early' "$DLOG"); ANSWERED=$(count 'bogus save status 200' "$CLOG")
|
||||
echo "[hammer] $DNAME: saves requested $SAVES, cooldown exits $EXITS, crash markers $CRASHES, after the AP came up: boot banners $BANNERS, port losses $LOSSES, reboots $REBOOTS; STA addresses $STA"
|
||||
FAILED=$(count 'bogus save status -1' "$CLOG")
|
||||
echo "[hammer] $CNAME: bogus saves answered $ANSWERED, failed $FAILED, real save status ${REAL:-none}"
|
||||
# Only whole messages, matched where the message starts, so a credential line or an odd SSID never gets in
|
||||
LC_ALL=C grep -a -E "^\[[0-9:]+\](\[[A-Z]\]\[[a-z_]+:[0-9]+\]: *(Exiting cooldown early|Starting fallback AP|Captive portal|Requested WiFi Settings Change|Retry phase|Connected\$|IP Address: )|\[capture\] port lost|($CRASH_RE))" "$DLOG" | LC_ALL=C tr -cd '\11\12\15\40-\176' | sed -E 's/(IP Address: )[^ ]+/\1<redacted>/' | cut -c1-110 | head -80 > "$DDIR/$DNAME-events.txt"
|
||||
echo "[hammer] events: $DDIR/$DNAME-events.txt"
|
||||
stop_tails
|
||||
[ -z "$INVALID" ] || { echo "[hammer] RESULT INVALID: $INVALID"; exit 2; }
|
||||
# Provisioning is proven by the device joining the network; the client's answer to the real save can time
|
||||
# out because the device leaves the AP before replying, so its status is information only
|
||||
if [ "$CRASHES" -gt 0 ] || [ "$REBOOTS" -gt 0 ] || [ "$STA" -lt 1 ]; then
|
||||
echo "[hammer] RESULT FAIL: crash markers $CRASHES, reboots $REBOOTS, STA addresses $STA, real save status ${REAL:-none}"; exit 3
|
||||
fi
|
||||
# A pass needs the stimulus the flow is about: at least half the configured bogus saves answered and one cooldown exit
|
||||
CONFIGURED=$(LC_ALL=C grep -a -E '^[[:space:]]*bogus_saves:' "$CYAML" | head -1 | tr -dc '0-9'); CONFIGURED=${CONFIGURED:-15}
|
||||
if [ "$EXITS" -lt 1 ] || [ $((ANSWERED * 2)) -lt "$CONFIGURED" ]; then
|
||||
echo "[hammer] RESULT INVALID: insufficient stimulus (bogus saves answered $ANSWERED of $CONFIGURED, cooldown exits $EXITS)"; exit 2
|
||||
fi
|
||||
[ "$FAILED" -eq 0 ] || echo "[hammer] WARNING: $FAILED of $CONFIGURED bogus saves got no answer"
|
||||
echo "[hammer] RESULT PASS"; exit 0
|
||||
@@ -1,77 +0,0 @@
|
||||
# Package for the client board: it joins the device under test's AP (priority 10), posts
|
||||
# `bogus_saves` bogus /wifisave requests `save_interval` apart, then posts the real credentials once.
|
||||
# Include from a per-run yaml that holds the platform block and the substitutions (example-client.yaml);
|
||||
# real_ssid and real_psk come from the scratch directory's secrets.yaml and must be URL safe.
|
||||
# Set bogus_saves to 1 for a plain provisioning check. Log lines the driver script counts:
|
||||
# "bogus save N", "bogus save status 200", "posting the real credentials", "real save status 200"
|
||||
# The hammer is a single-mode script, so a reconnect to the AP (the device under test rebooted) does not
|
||||
# start a second series on top of the first.
|
||||
substitutions:
|
||||
ap_password: hwtestpass
|
||||
bogus_saves: "15"
|
||||
save_interval: 3s
|
||||
bogus_ssid: hwtest-no-such-network
|
||||
bogus_psk: nosuchpassword
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
http_request:
|
||||
id: http
|
||||
verify_ssl: false
|
||||
timeout: 10s
|
||||
|
||||
wifi:
|
||||
id: wifi_id
|
||||
networks:
|
||||
- ssid: ${ap_ssid}
|
||||
password: ${ap_password}
|
||||
priority: 10
|
||||
- ssid: ${real_ssid}
|
||||
password: ${real_psk}
|
||||
on_connect:
|
||||
- lambda: |-
|
||||
char b[40];
|
||||
ESP_LOGI("test", "connected, ip %s", id(wifi_id).get_ip_addresses()[0].str_to(b));
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
char b[40];
|
||||
return strncmp(id(wifi_id).get_ip_addresses()[0].str_to(b), "192.168.4.", 10) == 0;
|
||||
then:
|
||||
- script.execute: hammer
|
||||
|
||||
script:
|
||||
- id: hammer
|
||||
mode: single
|
||||
then:
|
||||
- delay: 3s
|
||||
- logger.log: "hammering /wifisave with bogus credentials"
|
||||
- repeat:
|
||||
count: ${bogus_saves}
|
||||
then:
|
||||
- lambda: ESP_LOGI("test", "bogus save %u", (unsigned) iteration + 1);
|
||||
- http_request.get:
|
||||
url: "http://192.168.4.1/wifisave?ssid=${bogus_ssid}&psk=${bogus_psk}"
|
||||
capture_response: true
|
||||
on_response:
|
||||
- lambda: ESP_LOGI("test", "bogus save status %d", response->status_code);
|
||||
on_error:
|
||||
- lambda: ESP_LOGW("test", "bogus save status -1");
|
||||
- delay: ${save_interval}
|
||||
- logger.log: "posting the real credentials"
|
||||
- http_request.get:
|
||||
url: "http://192.168.4.1/wifisave?ssid=${real_ssid}&psk=${real_psk}"
|
||||
capture_response: true
|
||||
on_response:
|
||||
- lambda: ESP_LOGI("test", "real save status %d", response->status_code);
|
||||
on_error:
|
||||
- lambda: ESP_LOGW("test", "real save status -1");
|
||||
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: ESP_LOGI("test", "alive %u s", (unsigned) (millis() / 1000));
|
||||
@@ -1,31 +0,0 @@
|
||||
# Package for a device under test that never finds its network, falls back to its AP and serves the
|
||||
# captive portal. Include it from a per-run yaml that holds the platform block and the substitutions
|
||||
# (see example-dut.yaml). Bump `name` on every run: saved credentials are keyed by it, and a reused
|
||||
# name boots straight onto the real network and never raises the AP.
|
||||
substitutions:
|
||||
ap_password: hwtestpass
|
||||
ap_timeout: 15s
|
||||
bogus_ssid: hwtest-no-such-network
|
||||
bogus_psk: nosuchpassword
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
networks:
|
||||
- ssid: ${bogus_ssid}
|
||||
password: ${bogus_psk}
|
||||
ap:
|
||||
ssid: ${ap_ssid}
|
||||
password: ${ap_password}
|
||||
ap_timeout: ${ap_timeout}
|
||||
|
||||
captive_portal:
|
||||
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: ESP_LOGI("test", "alive %u s", (unsigned) (millis() / 1000));
|
||||
@@ -1,18 +0,0 @@
|
||||
# Client board for the /wifisave hammer. Copy into a scratch directory next to a secrets.yaml;
|
||||
# ap_ssid must match the device under test, real_ssid and real_psk must be URL safe.
|
||||
substitutions:
|
||||
name: hwtest-client-1
|
||||
ap_ssid: hwtest-ap
|
||||
real_ssid: !secret wifi_ssid
|
||||
real_psk: !secret wifi_password
|
||||
bogus_saves: "15"
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# esp8266: {board: d1_mini}
|
||||
|
||||
packages:
|
||||
hwtest: !include client-wifisave.yaml
|
||||
@@ -1,16 +0,0 @@
|
||||
# Device under test for the /wifisave hammer. Copy into a scratch directory, keep the file name equal
|
||||
# to `name`, pick one platform block, bump `name` every run.
|
||||
substitutions:
|
||||
name: hwtest-dut-1
|
||||
ap_ssid: hwtest-ap
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
# esp32: {board: esp32dev, framework: {type: esp-idf}}
|
||||
# rtl87xx: {board: bw15}
|
||||
# bk72xx: {board: generic-bk7238} # plus logger: {hardware_uart: UART1}, BK72xx logs on UART2 by default
|
||||
# rp2040: {board: rpipico2w}
|
||||
|
||||
packages:
|
||||
hwtest: !include dut-fallback-ap.yaml
|
||||
@@ -32,7 +32,7 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
|
||||
- name: Upload compile-test image artifact
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
# The tar is already compressed, so upload it as-is. archive: false
|
||||
# skips the redundant zip and makes the file name the artifact name
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Download image artifact
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: compile-test-image.tar.zst
|
||||
- name: Load image
|
||||
|
||||
+23
-40
@@ -49,7 +49,7 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -81,8 +81,8 @@ jobs:
|
||||
- name: Install apt packages (cached)
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
|
||||
determine-jobs:
|
||||
name: Determine which jobs to run
|
||||
@@ -395,8 +395,6 @@ jobs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
# A hung test is failed by pytest's --timeout below; this bounds the rest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -417,8 +415,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
@@ -440,7 +438,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -473,14 +471,13 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
||||
# A hung test fails after 30 min: the 15 min shared build lock wait plus a cold compile
|
||||
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal --timeout=1800 \
|
||||
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
|
||||
--junitxml=junit-integration.xml "${test_files[@]}"
|
||||
- name: Upload test artifacts
|
||||
# Tests that compare rendered output write the image they actually got here, so a
|
||||
# failure can be looked at without reproducing the whole build locally.
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: integration-test-artifacts-${{ matrix.bucket.name }}
|
||||
path: test_artifacts/
|
||||
@@ -491,7 +488,7 @@ jobs:
|
||||
# script/update_integration_test_durations.py; only full matrix dev
|
||||
# runs produce usable data.
|
||||
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: junit-integration-${{ strategy.job-index }}
|
||||
path: junit-integration.xml
|
||||
@@ -531,7 +528,7 @@ jobs:
|
||||
script/check_import_time.py --check --har importtime.har
|
||||
- name: Upload waterfall HAR
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: import-time-waterfall
|
||||
path: importtime.har
|
||||
@@ -629,7 +626,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
|
||||
uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -683,7 +680,7 @@ jobs:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP8266
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||
pio_cache_key: tidyesp8266
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino
|
||||
@@ -696,16 +693,12 @@ jobs:
|
||||
ignore_errors: false
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for RP2
|
||||
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER
|
||||
options: --environment rp2-tidy --grep USE_RP2
|
||||
pio_cache_key: tidyrp2
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for host
|
||||
options: --environment host-tidy --grep USE_HOST
|
||||
pio_cache_key: tidyhost
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for LibreTiny
|
||||
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
|
||||
pio_cache_key: tidylibretiny
|
||||
|
||||
steps:
|
||||
@@ -714,16 +707,6 @@ jobs:
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
- name: Install apt packages (cached)
|
||||
if: matrix.pio_cache_key == 'tidyhost'
|
||||
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
|
||||
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
|
||||
# exactly.
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
@@ -1006,14 +989,14 @@ jobs:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 S3
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM --grep USE_OPENTHREAD
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
|
||||
tidy_environment: esp32s3-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 P4
|
||||
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
|
||||
# so their code paths differ -- lint them under the P4 build too.
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
tidy_environment: esp32p4-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 C6
|
||||
@@ -1119,8 +1102,8 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev libmbedtls-dev ccache
|
||||
version: 1.2
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -1465,7 +1448,7 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -1664,7 +1647,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -1727,7 +1710,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -1757,13 +1740,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -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@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
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@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
days-before-close: 7
|
||||
stale-label: stale
|
||||
exempt-label: not-stale
|
||||
conflict-label: has-conflicts
|
||||
conflict-label: needs-rebase
|
||||
ignored-users: esphbot,codecov-commenter
|
||||
stale-pr-message: >
|
||||
There hasn't been any activity on this pull request recently. This
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -10,7 +10,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.17.0
|
||||
rev: v0.16.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -36,7 +36,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.22.0
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
|
||||
@@ -144,9 +144,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
|
||||
* **C++ Preprocessor Directives:**
|
||||
* **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
|
||||
* **A constant with a compile-time initializer is `static constexpr`,** not `static const`, at namespace scope
|
||||
and inside functions; `static const` is only for a value computed at run time. A function-local
|
||||
`static constexpr` table is a static constant to clang-tidy, so it is `UPPER_SNAKE_CASE` too.
|
||||
* **Use `#define` only for:**
|
||||
- Conditional compilation (`#ifdef`, `#ifndef`)
|
||||
- Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)
|
||||
@@ -220,20 +217,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
listener base (a second vptr per entity).
|
||||
* **A global feature define** says the code is compiled in, not that this instance uses it: keep the
|
||||
per-instance check (`if (this->aux_device_ == ...)`) inside the `#ifdef`.
|
||||
* **Model-specific code** sits behind a define the Python side emits only for configs that use that model
|
||||
(`cg.add_define("USE_SELEC_METER_EM4M")`), so the other models' builds leave it out; the runtime model check
|
||||
stays inside the `#ifdef`.
|
||||
* **Failures after `setup()`:** `mark_failed()` is for `setup()` only. A runtime read or write failure
|
||||
sets `status_set_warning()` and the next success clears it; writes skip work when `is_failed()`.
|
||||
* **`dump_config()` already prints the failure reason:** for a failed component, core prints
|
||||
`<component> is marked FAILED: <reason>` right after the component's `dump_config()`, using the reason
|
||||
passed to `mark_failed()`. So `dump_config()` never checks `is_failed()` to log a failure, and no
|
||||
`error_code_` member exists only to pick that message. Pass the reason where setup fails instead:
|
||||
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))` for a failed bus transfer, a short specific reason
|
||||
otherwise (`LOG_STR("Wrong chip ID")`). Core also logs it at that moment, so no `ESP_LOGE` repeats it
|
||||
before `mark_failed()`; keep a separate line only when it prints a value read at run time, such as the
|
||||
chip ID that did not match. Only the first reason is kept, so do not call `status_set_error()` before
|
||||
`mark_failed()` and do not mark the component failed again after a helper already did.
|
||||
* **Fixed platform facts compile to nothing:** gate on `#ifdef USE_ESP8266` and friends, not on a
|
||||
runtime check of something the build already knows.
|
||||
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
|
||||
@@ -287,8 +272,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
`from esphome.components.my_component import DOMAIN`. Keep it in alphabetical order with the other
|
||||
metadata constants. CI (`lint_component_domain`) fails without it.
|
||||
- `DEPENDENCIES`: List of required components
|
||||
- `AUTO_LOAD`: Components to automatically load. Not a way to put entities under the component's
|
||||
own key; see "Entities of a hub" below.
|
||||
- `AUTO_LOAD`: Components to automatically load
|
||||
- `CONFLICTS_WITH`: Incompatible components
|
||||
- `CODEOWNERS`: GitHub usernames responsible for maintenance
|
||||
- `MULTI_CONF`: Whether multiple instances are allowed
|
||||
@@ -380,26 +364,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
var = await switch.new_switch(config)
|
||||
```
|
||||
|
||||
- **Entities of a hub:** a new component's entities go in platform files (`sensor.py`,
|
||||
`binary_sensor.py`, ...) that take the hub's id, never under the hub's own key with their platforms
|
||||
added to `AUTO_LOAD` to make that work. Auto loading a platform because the C++ uses its type is
|
||||
fine. See https://developers.esphome.io/architecture/components/#exposing-multiple-entities.
|
||||
```yaml
|
||||
# Bad - entities under the hub's key
|
||||
my_hub:
|
||||
connected:
|
||||
name: Connected
|
||||
|
||||
# Good - the hub, then one platform entry that holds its entities
|
||||
my_hub:
|
||||
id: hub1
|
||||
binary_sensor:
|
||||
- platform: my_hub
|
||||
my_hub_id: hub1 # optional with a single hub
|
||||
connected:
|
||||
name: Connected
|
||||
```
|
||||
|
||||
- **Optional child entities of a hub:** bind the config once with `sensor.sub_sensors(config)` (or
|
||||
`sub_binary_sensors`, `sub_text_sensors`, `sub_buttons`, `sub_switches`, `sub_numbers`,
|
||||
`sub_selects` in their domains), adding `parent=hub` for entities that derive from `Parented<T>`,
|
||||
@@ -410,25 +374,15 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
itself, `hub` for one fetched with `cg.get_variable`) so the calls fit on one line. Loops whose
|
||||
setter also takes an index, such as `set_gate_threshold(x, n)`, stay as they are.
|
||||
```python
|
||||
# my_hub/sensor.py
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, hub.set_temperature_sensor)
|
||||
await sensors(CONF_HUMIDITY, hub.set_humidity_sensor)
|
||||
|
||||
|
||||
# my_hub/button.py
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
|
||||
buttons = button.sub_buttons(config, parent=hub)
|
||||
await buttons(CONF_RESTART, hub.set_restart_button)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
|
||||
buttons = button.sub_buttons(config, parent=var)
|
||||
await buttons(CONF_RESTART, var.set_restart_button)
|
||||
```
|
||||
|
||||
- **Pins and referenced components:** set them with `await cg.add_pin(var.set_cs_pin, config[CONF_CS_PIN])`
|
||||
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`.
|
||||
|
||||
* **Automations (Triggers, Actions, Conditions):**
|
||||
|
||||
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
|
||||
@@ -613,9 +567,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
An intentionally empty body (an unused `register_dumper` builder, for example) gets a one-line docstring, not `pass`.
|
||||
|
||||
* **Configuration Validation:**
|
||||
* **One schema per model:** when a component's options depend on a `model` key, use `cv.typed_schema({...},
|
||||
key=CONF_MODEL, default_type=...)` with each model's own keys and defaults, not one schema plus a validator
|
||||
that rejects keys and fills defaults afterwards.
|
||||
* **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`.
|
||||
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
|
||||
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
|
||||
@@ -658,9 +609,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
`on_tx` lambda with a static counter and `inject_to_rx_buffer` (see `uart_mock_ld2412_requery_after_giveup.yaml`).
|
||||
* **Keep build-test variants few:** one `test.<platform>.yaml` per platform the code actually differs
|
||||
on. A variant per board for platform-identical code only adds CI time.
|
||||
* **A test that needs a shared component in a particular state** (`esp32_ble` security settings, an option
|
||||
that PSRAM rejects) cannot be grouped: the merged block wins and the whole batch fails. Add the component to
|
||||
`ISOLATED_COMPONENTS` in `script/analyze_component_buses.py`, or move the option to a `test-<variant>` file.
|
||||
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
|
||||
```
|
||||
tests/
|
||||
|
||||
+1
-30
@@ -30,9 +30,6 @@ esphome/components/airthings_ble/* @jeromelaban
|
||||
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
|
||||
esphome/components/airthings_wave_mini/* @ncareau
|
||||
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
||||
esphome/components/airton/* @Procsiab
|
||||
esphome/components/airton/select/* @Procsiab
|
||||
esphome/components/airton/switch/* @Procsiab
|
||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
||||
esphome/components/alpha3/* @jan-hofmeier
|
||||
esphome/components/am2315c/* @swoboda1337
|
||||
@@ -43,7 +40,6 @@ esphome/components/analog_threshold/* @ianchi
|
||||
esphome/components/animation/* @syndlex
|
||||
esphome/components/anova/* @buxtronix
|
||||
esphome/components/apc1/* @ademuri
|
||||
esphome/components/apc_proteous/* @clydebarrow
|
||||
esphome/components/apds9306/* @aodrenah
|
||||
esphome/components/apds9930/* @iyesin
|
||||
esphome/components/api/* @esphome/core
|
||||
@@ -52,7 +48,6 @@ esphome/components/aranet/* @patrickod
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
esphome/components/as734x/* @latonita @mrgnr
|
||||
esphome/components/async_tcp/* @esphome/core
|
||||
esphome/components/at581x/* @X-Ryl669
|
||||
esphome/components/atc_mithermometer/* @ahpohl
|
||||
@@ -160,10 +155,6 @@ esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dew_point/* @CFlix
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
esphome/components/dfrobot_sen0465_base/* @tzabbi
|
||||
esphome/components/dfrobot_sen0465_i2c/* @tzabbi
|
||||
esphome/components/dfrobot_sen0465_uart/* @tzabbi
|
||||
esphome/components/dfrobot_sen0609/* @remcom
|
||||
esphome/components/dht/* @OttoWinter
|
||||
esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
||||
@@ -192,7 +183,6 @@ esphome/components/es7243e/* @kbx81
|
||||
esphome/components/es8156/* @kbx81
|
||||
esphome/components/es8311/* @kahrendt @kroimon
|
||||
esphome/components/es8388/* @P4uLT
|
||||
esphome/components/es9038q2m_katana/* @GioMez
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
|
||||
esphome/components/esp32_ble_client/* @bdraco @jesserockz
|
||||
@@ -222,14 +212,12 @@ esphome/components/factory_reset/* @anatoly-savchenkov
|
||||
esphome/components/fastled_base/* @OttoWinter
|
||||
esphome/components/feedback/* @ianchi
|
||||
esphome/components/file/* @esphome/core
|
||||
esphome/components/filter_lifetime/* @tuct
|
||||
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
||||
esphome/components/font/* @clydebarrow @esphome/core
|
||||
esphome/components/friedrich/* @rwrozelle
|
||||
esphome/components/fs3000/* @kahrendt
|
||||
esphome/components/ft5x06/* @clydebarrow
|
||||
esphome/components/ft63x6/* @gpambrozio
|
||||
esphome/components/fusb302b/* @remcom
|
||||
esphome/components/gcja5/* @gcormier
|
||||
esphome/components/gdk101/* @Szewcson
|
||||
esphome/components/gl_r01_i2c/* @pkejval
|
||||
@@ -251,14 +239,9 @@ esphome/components/gt911/* @clydebarrow @jesserockz
|
||||
esphome/components/haier/* @paveldn
|
||||
esphome/components/haier/binary_sensor/* @paveldn
|
||||
esphome/components/haier/button/* @paveldn
|
||||
esphome/components/haier/select/* @paveldn
|
||||
esphome/components/haier/sensor/* @paveldn
|
||||
esphome/components/haier/switch/* @paveldn
|
||||
esphome/components/haier/text_sensor/* @paveldn
|
||||
esphome/components/haier_tundra/* @fauxpark
|
||||
esphome/components/haier_tundra/button/* @fauxpark
|
||||
esphome/components/haier_tundra/switch/* @fauxpark
|
||||
esphome/components/haier_water_heater/* @TheLexus
|
||||
esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
@@ -267,7 +250,6 @@ esphome/components/hc8/* @omartijn
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/hdc2080/* @G-Pereira @jesserockz
|
||||
esphome/components/hdc302x/* @joshuasing
|
||||
esphome/components/hdmi_cec/* @JosVanEijndhoven
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
@@ -304,7 +286,6 @@ esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/i2s_clockless_led_strip/* @j9brown
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
esphome/components/icm20689/* @lboue
|
||||
esphome/components/icnt86/* @danepowell
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
esphome/components/improv_base/* @esphome/core
|
||||
@@ -397,10 +378,8 @@ esphome/components/micro_wake_word/* @jesserockz @kahrendt
|
||||
esphome/components/micronova/* @edenhaus @jorre05
|
||||
esphome/components/microphone/* @jesserockz @kahrendt
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/mics_5524/* @nliaudat
|
||||
esphome/components/midea/* @dudanov
|
||||
esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/midea_portasplit/* @Fexiven
|
||||
esphome/components/mipi_dsi/* @clydebarrow
|
||||
esphome/components/mipi_rgb/* @clydebarrow
|
||||
esphome/components/mipi_spi/* @clydebarrow
|
||||
@@ -431,9 +410,6 @@ esphome/components/mpl3115a2/* @kbickar
|
||||
esphome/components/mpu6886/* @fabaff
|
||||
esphome/components/ms8607/* @e28eta
|
||||
esphome/components/msa3xx/* @latonita
|
||||
esphome/components/mt6701/* @slimcdk
|
||||
esphome/components/mt6701_i2c/* @slimcdk
|
||||
esphome/components/mt6701_spi/* @slimcdk
|
||||
esphome/components/nau7802/* @cujomalainey
|
||||
esphome/components/network/* @esphome/core
|
||||
esphome/components/nextion/* @edwardtfn @senexcrenshaw
|
||||
@@ -458,7 +434,6 @@ esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9505/* @ajfriesen @ssieb
|
||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
@@ -518,12 +493,10 @@ esphome/components/rp2040_pwm/* @jesserockz
|
||||
esphome/components/rp2_ble_tracker/* @bdraco
|
||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtsp_server/* @max246
|
||||
esphome/components/rtttl/* @glmnet @ximex
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8025t/* @remcom
|
||||
esphome/components/rx8111/* @techdregs
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/samsung/* @jorofi
|
||||
@@ -691,11 +664,10 @@ esphome/components/vbus/* @ssieb
|
||||
esphome/components/veml3235/* @kbx81
|
||||
esphome/components/veml7700/* @latonita
|
||||
esphome/components/version/* @esphome/core
|
||||
esphome/components/vevor_dtx/* @Bofr5555
|
||||
esphome/components/voice_assistant/* @jesserockz @kahrendt
|
||||
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
esphome/components/water_heater/* @esphome/core
|
||||
esphome/components/water_heater/* @dhoeben
|
||||
esphome/components/waveshare_epaper/* @clydebarrow
|
||||
esphome/components/waveshare_io_ch32v003/* @latonita
|
||||
esphome/components/web_server/ota/* @esphome/core
|
||||
@@ -734,7 +706,6 @@ esphome/components/xl9535/* @mreditor97
|
||||
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
||||
esphome/components/xxtea/* @clydebarrow
|
||||
esphome/components/zephyr/* @tomaszduda23
|
||||
esphome/components/zephyr_coredump/* @tomaszduda23
|
||||
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
|
||||
esphome/components/zephyr_pwm/* @wiomoc
|
||||
esphome/components/zhlt01/* @cfeenstra1024
|
||||
|
||||
+1
-1
@@ -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.23.1
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+18
-16
@@ -483,9 +483,12 @@ def has_ota() -> bool:
|
||||
|
||||
def has_native_ota() -> bool:
|
||||
"""Check if native API OTA upload is available (``platform: esphome``)."""
|
||||
from esphome.espota2 import esphome_ota_items
|
||||
|
||||
return bool(esphome_ota_items(CORE.config))
|
||||
if CONF_OTA not in CORE.config:
|
||||
return False
|
||||
return any(
|
||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
for ota_item in CORE.config[CONF_OTA]
|
||||
)
|
||||
|
||||
|
||||
def has_web_server_ota() -> bool:
|
||||
@@ -1356,17 +1359,14 @@ def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
|
||||
return CONF_WEB_SERVER
|
||||
|
||||
|
||||
def _esphome_ota_conf(config: ConfigType) -> ConfigType | None:
|
||||
"""The validated esphome OTA platform block, if configured."""
|
||||
from esphome.espota2 import esphome_ota_items
|
||||
|
||||
return next(iter(esphome_ota_items(config)), None)
|
||||
|
||||
|
||||
def _upload_via_native_api(
|
||||
config: ConfigType, network_devices: list[str], args: ArgsProtocol
|
||||
) -> tuple[int, str | None]:
|
||||
ota_conf = _esphome_ota_conf(config) or {}
|
||||
ota_conf: ConfigType = {}
|
||||
for ota_item in config.get(CONF_OTA, []):
|
||||
if ota_item.get(CONF_PLATFORM) == CONF_ESPHOME:
|
||||
ota_conf = ota_item
|
||||
break
|
||||
|
||||
from esphome import espota2
|
||||
from esphome.components.noise import static_encryption_key
|
||||
@@ -1457,8 +1457,10 @@ def _old_key_notice(config: ConfigType) -> None:
|
||||
`upload` may send a stale image that still carries the previous key."""
|
||||
from esphome import espota2
|
||||
|
||||
for item in espota2.esphome_ota_items(config):
|
||||
if (item.get(CONF_ENCRYPTION) or {}).get(espota2.CONF_OLD_KEY):
|
||||
for item in config.get(CONF_OTA, []):
|
||||
if item.get(CONF_PLATFORM) == CONF_ESPHOME and (
|
||||
item.get(CONF_ENCRYPTION) or {}
|
||||
).get(espota2.CONF_OLD_KEY):
|
||||
_LOGGER.warning(espota2.OLD_KEY_REMOVE_NOTICE)
|
||||
|
||||
|
||||
@@ -1466,12 +1468,12 @@ def _upload_via_web_server(
|
||||
config: ConfigType, network_devices: list[str], binary: Path
|
||||
) -> tuple[int, str | None]:
|
||||
from esphome import web_server_ota
|
||||
from esphome.espota2 import esphome_ota_items
|
||||
from esphome.web_server_helpers import get_web_server_connection
|
||||
|
||||
if any(
|
||||
ota_item.get(CONF_ENCRYPTION) is not None
|
||||
for ota_item in esphome_ota_items(config)
|
||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
and ota_item.get(CONF_ENCRYPTION) is not None
|
||||
for ota_item in config.get(CONF_OTA, [])
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"This config has OTA encryption, but the web_server OTA path sends "
|
||||
|
||||
+4
-142
@@ -14,7 +14,6 @@ from esphome.const import (
|
||||
CONF_COUNT,
|
||||
CONF_ELSE,
|
||||
CONF_ID,
|
||||
CONF_IF,
|
||||
CONF_THEN,
|
||||
CONF_TIME,
|
||||
CONF_TIMEOUT,
|
||||
@@ -35,9 +34,6 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
from esphome.util import Registry
|
||||
|
||||
CONF_CHOOSE = "choose"
|
||||
CONF_CHOICE_ID = "choice_id"
|
||||
|
||||
|
||||
def progmem_bytes(name: str, data: bytes | list[int]) -> MockObj:
|
||||
"""Shared PROGMEM table for constant bytes; equal payloads share one, empty is nullptr."""
|
||||
@@ -844,39 +840,10 @@ async def delay_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def _wrap_bare_if_condition(value: Any) -> Any:
|
||||
"""Allow a single condition directly under `if:` in place of `condition: {...}`."""
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
# `any` and `all` are also registered conditions, but here they are the if action's own keys
|
||||
bare = [
|
||||
key
|
||||
for key in value
|
||||
if key in CONDITION_REGISTRY and key not in (CONF_ANY, CONF_ALL)
|
||||
]
|
||||
if not bare:
|
||||
return value
|
||||
if len(bare) > 1:
|
||||
raise cv.Invalid(
|
||||
f"Only one condition may be given directly under 'if', found {', '.join(bare)}. "
|
||||
f"Use '{CONF_CONDITION}', '{CONF_ALL}' or '{CONF_ANY}' to combine conditions"
|
||||
)
|
||||
if other := [k for k in (CONF_CONDITION, CONF_ANY, CONF_ALL) if k in value]:
|
||||
raise cv.Invalid(
|
||||
f"'{bare[0]}' given directly under 'if' cannot be used with '{other[0]}'. "
|
||||
f"Move it into '{other[0]}'"
|
||||
)
|
||||
key = bare[0]
|
||||
result = {k: v for k, v in value.items() if k != key}
|
||||
result[CONF_CONDITION] = {key: value[key]}
|
||||
return result
|
||||
|
||||
|
||||
@register_action(
|
||||
"if",
|
||||
IfAction,
|
||||
cv.All(
|
||||
_wrap_bare_if_condition,
|
||||
{
|
||||
cv.Exclusive(
|
||||
CONF_CONDITION, CONF_CONDITION
|
||||
@@ -903,104 +870,14 @@ async def if_action_to_code(
|
||||
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
|
||||
condition = await build_condition(config[cond_conf], template_arg, args)
|
||||
var = cg.new_Pvariable(action_id, if_template_arg, condition)
|
||||
then_actions = else_actions = None
|
||||
if CONF_THEN in config:
|
||||
then_actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
||||
if has_else:
|
||||
else_actions = await build_action_list(config[CONF_ELSE], template_arg, args)
|
||||
_add_if_branches(var, then_actions, else_actions)
|
||||
return var
|
||||
|
||||
|
||||
def _add_if_branches(
|
||||
var: MockObj,
|
||||
then_actions: list[MockObj] | None,
|
||||
else_actions: list[MockObj] | None,
|
||||
) -> None:
|
||||
"""Add the branches of an IfAction one action at a time; None leaves a branch out."""
|
||||
if then_actions is not None:
|
||||
for action in then_actions:
|
||||
for action in await build_action_list(config[CONF_THEN], template_arg, args):
|
||||
cg.add(var.add_then_action(action))
|
||||
cg.add(var.finish_then())
|
||||
if else_actions is not None:
|
||||
for action in else_actions:
|
||||
if has_else:
|
||||
for action in await build_action_list(config[CONF_ELSE], template_arg, args):
|
||||
cg.add(var.add_else_action(action))
|
||||
cg.add(var.finish_else())
|
||||
|
||||
|
||||
def _validate_choice(choice: ConfigType) -> ConfigType:
|
||||
"""A choice is either `if` with `then`, or `else` on its own."""
|
||||
if CONF_ELSE in choice:
|
||||
for key in (CONF_IF, CONF_THEN):
|
||||
if key in choice:
|
||||
raise cv.Invalid(f"'{key}' cannot be used with 'else'", path=[key])
|
||||
return choice
|
||||
for key in (CONF_IF, CONF_THEN):
|
||||
if key not in choice:
|
||||
raise cv.Invalid(f"'{key}' is required", path=[key])
|
||||
return choice
|
||||
|
||||
|
||||
def _validate_choose_else(choices: list[ConfigType]) -> list[ConfigType]:
|
||||
"""`else` may only be the last choice, and not the only one."""
|
||||
for index, choice in enumerate(choices[:-1]):
|
||||
if CONF_ELSE in choice:
|
||||
raise cv.Invalid(
|
||||
"Only the last choice may be 'else'", path=[index, CONF_ELSE]
|
||||
)
|
||||
if len(choices) == 1 and CONF_ELSE in choices[0]:
|
||||
raise cv.Invalid(
|
||||
"At least one choice must have 'if' and 'then'", path=[0, CONF_ELSE]
|
||||
)
|
||||
return choices
|
||||
|
||||
|
||||
@register_action(
|
||||
CONF_CHOOSE,
|
||||
IfAction,
|
||||
cv.All(
|
||||
cv.ensure_list(
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_CHOICE_ID): cv.declare_id(IfAction),
|
||||
cv.Optional(CONF_IF): validate_potentially_and_condition,
|
||||
cv.Optional(CONF_THEN): validate_action_list,
|
||||
cv.Optional(CONF_ELSE): validate_action_list,
|
||||
}
|
||||
),
|
||||
_validate_choice,
|
||||
)
|
||||
),
|
||||
cv.Length(min=1),
|
||||
_validate_choose_else,
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def choose_action_to_code(
|
||||
config: list[ConfigType],
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
# Built as a chain of if/else actions: each choice's else branch holds the next choice,
|
||||
# and the last choice's else branch holds the `else` actions, if any.
|
||||
else_actions: list[MockObj] | None = None
|
||||
if CONF_ELSE in config[-1]:
|
||||
else_actions = await build_action_list(
|
||||
config[-1][CONF_ELSE], template_arg, args
|
||||
)
|
||||
config = config[:-1]
|
||||
var: MockObj | None = None
|
||||
for index in reversed(range(len(config))):
|
||||
choice = config[index]
|
||||
if_template_arg = cg.TemplateArguments(else_actions is not None, *template_arg)
|
||||
condition = await build_condition(choice[CONF_IF], template_arg, args)
|
||||
choice_id = action_id if index == 0 else choice[CONF_CHOICE_ID]
|
||||
var = cg.new_Pvariable(choice_id, if_template_arg, condition)
|
||||
then_actions = await build_action_list(choice[CONF_THEN], template_arg, args)
|
||||
_add_if_branches(var, then_actions, else_actions)
|
||||
else_actions = [var]
|
||||
return var
|
||||
|
||||
|
||||
@@ -1029,21 +906,6 @@ async def while_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def _shadow_iteration(args: TemplateArgsType) -> TemplateArgsType:
|
||||
"""Rename an inherited `iteration` so the inner repeat's own counter can shadow it."""
|
||||
names = {name for _, name in args}
|
||||
renamed: TemplateArgsType = []
|
||||
for type_, name in args:
|
||||
if name == "iteration":
|
||||
depth = 1
|
||||
while f"iteration_{depth}" in names:
|
||||
depth += 1
|
||||
name = f"iteration_{depth}"
|
||||
names.add(name)
|
||||
renamed.append((type_, name))
|
||||
return renamed
|
||||
|
||||
|
||||
@register_action(
|
||||
"repeat",
|
||||
RepeatAction,
|
||||
@@ -1067,7 +929,7 @@ async def repeat_action_to_code(
|
||||
actions = await build_action_list(
|
||||
config[CONF_THEN],
|
||||
cg.TemplateArguments(cg.uint32, *template_arg.args),
|
||||
[(cg.uint32, "iteration"), *_shadow_iteration(args)],
|
||||
[(cg.uint32, "iteration"), *args],
|
||||
)
|
||||
for action in actions:
|
||||
cg.add(var.add_then_action(action))
|
||||
|
||||
@@ -53,14 +53,11 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
with_local_variable,
|
||||
)
|
||||
from esphome.cpp_helpers import ( # noqa: F401
|
||||
add_pin,
|
||||
add_variable,
|
||||
build_registry_entry,
|
||||
build_registry_list,
|
||||
extract_registry_entry_config,
|
||||
get_slot_count,
|
||||
gpio_pin_expression,
|
||||
new_bus_component,
|
||||
past_safe_mode,
|
||||
register_component,
|
||||
register_parented,
|
||||
|
||||
@@ -23,6 +23,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await stepper.register_stepper(var, config)
|
||||
|
||||
await cg.add_pin(var.set_step_pin, config[CONF_STEP_PIN])
|
||||
await cg.add_pin(var.set_dir_pin, config[CONF_DIR_PIN])
|
||||
await cg.add_pin(var.set_sleep_pin, config.get(CONF_SLEEP_PIN))
|
||||
step_pin = await cg.gpio_pin_expression(config[CONF_STEP_PIN])
|
||||
cg.add(var.set_step_pin(step_pin))
|
||||
dir_pin = await cg.gpio_pin_expression(config[CONF_DIR_PIN])
|
||||
cg.add(var.set_dir_pin(dir_pin))
|
||||
|
||||
if sleep_pin_config := config.get(CONF_SLEEP_PIN):
|
||||
sleep_pin = await cg.gpio_pin_expression(sleep_pin_config)
|
||||
cg.add(var.set_sleep_pin(sleep_pin))
|
||||
|
||||
@@ -19,7 +19,11 @@ AbsoluteHumidityComponent = absolute_humidity_ns.class_(
|
||||
SaturationVaporPressureEquation = absolute_humidity_ns.enum(
|
||||
"SaturationVaporPressureEquation"
|
||||
)
|
||||
EQUATION = SaturationVaporPressureEquation.options("BUCK", "TETENS", "WOBUS")
|
||||
EQUATION = {
|
||||
"BUCK": SaturationVaporPressureEquation.BUCK,
|
||||
"TETENS": SaturationVaporPressureEquation.TETENS,
|
||||
"WOBUS": SaturationVaporPressureEquation.WOBUS,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
|
||||
@@ -14,9 +14,11 @@ ac_dimmer_ns = cg.esphome_ns.namespace("ac_dimmer")
|
||||
AcDimmer = ac_dimmer_ns.class_("AcDimmer", output.FloatOutput, cg.Component)
|
||||
|
||||
DimMethod = ac_dimmer_ns.enum("DimMethod")
|
||||
DIM_METHODS = DimMethod.options(
|
||||
"LEADING_PULSE", "LEADING", "TRAILING", prefix="DIM_METHOD_"
|
||||
)
|
||||
DIM_METHODS = {
|
||||
"LEADING_PULSE": DimMethod.DIM_METHOD_LEADING_PULSE,
|
||||
"LEADING": DimMethod.DIM_METHOD_LEADING,
|
||||
"TRAILING": DimMethod.DIM_METHOD_TRAILING,
|
||||
}
|
||||
|
||||
ZC_INTERRUPT_TYPES = {
|
||||
"RISING": gpio_ns.INTERRUPT_RISING_EDGE,
|
||||
@@ -68,8 +70,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
config[CONF_MIN_POWER] = 0.1
|
||||
await output.register_output(var, config)
|
||||
|
||||
await cg.add_pin(var.set_gate_pin, config[CONF_GATE_PIN])
|
||||
await cg.add_pin(var.set_zero_cross_pin, config[CONF_ZERO_CROSS_PIN])
|
||||
pin = await cg.gpio_pin_expression(config[CONF_GATE_PIN])
|
||||
cg.add(var.set_gate_pin(pin))
|
||||
pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
|
||||
cg.add(var.set_zero_cross_pin(pin))
|
||||
cg.add(var.set_zero_cross_interrupt_type(config[CONF_ZERO_CROSS_INTERRUPT_TYPE]))
|
||||
cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE]))
|
||||
cg.add(var.set_method(config[CONF_METHOD]))
|
||||
|
||||
@@ -46,7 +46,11 @@ ATTENUATION_MODES = {
|
||||
|
||||
sampling_mode = adc_ns.enum("SamplingMode", is_class=True)
|
||||
|
||||
SAMPLING_MODES = sampling_mode.options("avg", "min", "max")
|
||||
SAMPLING_MODES = {
|
||||
"avg": sampling_mode.AVG,
|
||||
"min": sampling_mode.MIN,
|
||||
"max": sampling_mode.MAX,
|
||||
}
|
||||
|
||||
adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True)
|
||||
|
||||
|
||||
@@ -164,7 +164,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Remove before 2027.2.0
|
||||
cg.add(var.set_is_temperature())
|
||||
elif not CORE.is_nrf52 or config[CONF_PIN][CONF_NUMBER] not in EXTRA_ADC:
|
||||
await cg.add_pin(var.set_pin, config[CONF_PIN])
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
# Skip the setters when the config matches the C++ initializers.
|
||||
if config[CONF_RAW]:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
@@ -19,4 +20,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await spi.new_spi_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
@@ -56,4 +56,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
cg.add(var.set_pixel_mapper(pixel_mapper_template_))
|
||||
|
||||
await display.add_writer(var, config)
|
||||
if lambda_config := config.get(CONF_LAMBDA):
|
||||
lambda_ = await cg.process_lambda(
|
||||
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
namespace esphome::ade7880 {
|
||||
|
||||
// adapted from https://stackoverflow.com/a/55912127/1886371
|
||||
template<size_t Bits, typename T> inline T sign_extend(const T &v) noexcept {
|
||||
using S = struct { signed Val : Bits; };
|
||||
return reinterpret_cast<const S *>(&v)->Val;
|
||||
}
|
||||
|
||||
// Register types
|
||||
// unsigned 8-bit (uint8_t)
|
||||
// signed 10-bit - 16-bit ZP on wire (int16_t, needs sign extension)
|
||||
@@ -43,7 +49,7 @@ uint16_t ADE7880::read_u16_register16_(uint16_t a_register) {
|
||||
|
||||
int32_t ADE7880::read_s24zp_register16_(uint16_t a_register) {
|
||||
// s24zp means 24 bit signed value in the lower 24 bits of a 32-bit register
|
||||
uint32_t in;
|
||||
int32_t in;
|
||||
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
|
||||
return sign_extend<24>(convert_big_endian(in));
|
||||
}
|
||||
|
||||
@@ -282,11 +282,20 @@ async def power_channel(config: ConfigType) -> MockObj:
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_irq0_pin, config.get(CONF_IRQ0_PIN))
|
||||
await cg.add_pin(var.set_irq1_pin, config[CONF_IRQ1_PIN])
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
if irq0_pin := config.get(CONF_IRQ0_PIN):
|
||||
pin = await cg.gpio_pin_expression(irq0_pin)
|
||||
cg.add(var.set_irq0_pin(pin))
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_IRQ1_PIN])
|
||||
cg.add(var.set_irq1_pin(pin))
|
||||
|
||||
if reset_pin := config.get(CONF_RESET_PIN):
|
||||
pin = await cg.gpio_pin_expression(reset_pin)
|
||||
cg.add(var.set_reset_pin(pin))
|
||||
|
||||
if frequency := config.get(CONF_FREQUENCY):
|
||||
cg.add(var.set_frequency(frequency))
|
||||
|
||||
@@ -169,7 +169,9 @@ ADE7953_CONFIG_SCHEMA = cv.Schema(
|
||||
async def register_ade7953(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await cg.add_pin(var.set_irq_pin, config.get(CONF_IRQ_PIN))
|
||||
if irq_pin_config := config.get(CONF_IRQ_PIN):
|
||||
irq_pin = await cg.gpio_pin_expression(irq_pin_config)
|
||||
cg.add(var.set_irq_pin(irq_pin))
|
||||
|
||||
cg.add(var.set_pga_v(PGA_GAINS[config.get(CONF_VOLTAGE_PGA_GAIN)]))
|
||||
cg.add(var.set_pga_ia(PGA_GAINS[config.get(CONF_CURRENT_PGA_GAIN_A)]))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -26,6 +27,8 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE]))
|
||||
|
||||
@@ -11,7 +11,7 @@ static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
|
||||
void ADS1115Component::setup() {
|
||||
uint16_t value;
|
||||
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ void ADS1115Component::setup() {
|
||||
config |= 0b0000000000000011;
|
||||
|
||||
if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->prev_config_ = config;
|
||||
@@ -66,6 +66,9 @@ void ADS1115Component::setup() {
|
||||
void ADS1115Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ADS1115:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain,
|
||||
ADS1115Resolution resolution, ADS1115Samplerate samplerate) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@solomondg1"]
|
||||
@@ -25,4 +26,6 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await spi.new_spi_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
@@ -4,6 +4,7 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_TVOC,
|
||||
CONF_VALUE,
|
||||
@@ -57,7 +58,9 @@ FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency=
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TVOC, var.set_tvoc)
|
||||
@@ -81,9 +84,11 @@ automation.register_apply_action(
|
||||
)
|
||||
|
||||
AGS10SetZeroPointActionMode = ags10_ns.enum("AGS10SetZeroPointActionMode")
|
||||
AGS10_SET_ZERO_POINT_ACTION_MODE = AGS10SetZeroPointActionMode.options(
|
||||
"FACTORY_DEFAULT", "CURRENT_VALUE", "CUSTOM_VALUE"
|
||||
)
|
||||
AGS10_SET_ZERO_POINT_ACTION_MODE = {
|
||||
"FACTORY_DEFAULT": AGS10SetZeroPointActionMode.FACTORY_DEFAULT,
|
||||
"CURRENT_VALUE": AGS10SetZeroPointActionMode.CURRENT_VALUE,
|
||||
"CUSTOM_VALUE": AGS10SetZeroPointActionMode.CUSTOM_VALUE,
|
||||
}
|
||||
|
||||
AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
{
|
||||
|
||||
@@ -52,7 +52,8 @@ void AHT10Component::setup() {
|
||||
break;
|
||||
}
|
||||
if (error_code != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint8_t cal_attempts = 0;
|
||||
@@ -60,17 +61,20 @@ void AHT10Component::setup() {
|
||||
while (data & AHT10_STATUS_BUSY) {
|
||||
delay(AHT10_DEFAULT_DELAY);
|
||||
if (this->read(&data, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
++cal_attempts;
|
||||
if (cal_attempts > AHT10_INIT_ATTEMPTS) {
|
||||
this->mark_failed(LOG_STR("Initialization timed out"));
|
||||
ESP_LOGE(TAG, "Initialization timed out");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
|
||||
this->mark_failed(LOG_STR("Initialization failed"));
|
||||
ESP_LOGE(TAG, "Initialization failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -148,6 +152,9 @@ void AHT10Component::update() {
|
||||
void AHT10Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AHT10:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_VARIANT,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
@@ -19,7 +20,10 @@ aht10_ns = cg.esphome_ns.namespace("aht10")
|
||||
AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice)
|
||||
|
||||
AHT10Variant = aht10_ns.enum("AHT10Variant")
|
||||
AHT10_VARIANTS = AHT10Variant.options("AHT10", "AHT20")
|
||||
AHT10_VARIANTS = {
|
||||
"AHT10": AHT10Variant.AHT10,
|
||||
"AHT20": AHT10Variant.AHT20,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -48,7 +52,9 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_variant(config[CONF_VARIANT]))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
|
||||
@@ -10,7 +10,8 @@ ESPHOME_LOG_TAG(TAG, "aic3204");
|
||||
|
||||
#define ERROR_CHECK(err, msg) \
|
||||
if (!(err)) { \
|
||||
this->mark_failed(LOG_STR(msg)); \
|
||||
ESP_LOGE(TAG, msg); \
|
||||
this->mark_failed(); \
|
||||
return; \
|
||||
}
|
||||
|
||||
@@ -107,6 +108,10 @@ void AIC3204::setup() {
|
||||
void AIC3204::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AIC3204:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
bool AIC3204::set_mute_off() {
|
||||
|
||||
@@ -3,7 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.audio_dac import AudioDac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MODE
|
||||
from esphome.const import CONF_ID, CONF_MODE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -41,4 +41,6 @@ automation.register_apply_action(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -29,7 +29,10 @@ AirthingsWavePlus = airthings_wave_plus_ns.class_(
|
||||
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
WaveDeviceType = airthings_wave_plus_ns.enum("WaveDeviceType")
|
||||
DEVICE_TYPES = WaveDeviceType.options("WAVE_PLUS", "WAVE_GEN2")
|
||||
DEVICE_TYPES = {
|
||||
"WAVE_PLUS": WaveDeviceType.WAVE_PLUS,
|
||||
"WAVE_GEN2": WaveDeviceType.WAVE_GEN2,
|
||||
}
|
||||
|
||||
|
||||
def validate_wave_gen2_config(config: ConfigType) -> ConfigType:
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
CODEOWNERS = ["@Procsiab"]
|
||||
DOMAIN = "airton"
|
||||
@@ -1,319 +0,0 @@
|
||||
#include "airton.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::airton {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "airton.climate");
|
||||
|
||||
// Distinct from the climate's own RESTORE_STATE_VERSION, which shares the entity's preference key
|
||||
static constexpr uint32_t RESTORE_SETTINGS_VERSION = 0x2E86A1F3UL;
|
||||
|
||||
// From IRutils.h of IRremoteESP8266 library
|
||||
static uint8_t checksum(const uint8_t *frame) {
|
||||
return static_cast<uint8_t>(0x7F - sum_bytes(frame, AIRTON_STATE_FRAME_SIZE - 1)) ^ 0x2C;
|
||||
}
|
||||
|
||||
void AirtonClimate::setup() {
|
||||
climate_ir::ClimateIR::setup();
|
||||
// The flap position is the one source of truth for the swing mode the climate state restored
|
||||
this->settings_pref_ = this->make_entity_preference<AirtonSettings>(RESTORE_SETTINGS_VERSION);
|
||||
this->settings_pref_.load(&this->settings_);
|
||||
this->sync_swing_mode_();
|
||||
this->publish_settings_();
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void AirtonClimate::publish_settings_() {
|
||||
#ifdef USE_SWITCH
|
||||
if (this->sleep_mode_switch_ != nullptr)
|
||||
this->sleep_mode_switch_->publish_state(this->settings_.sleep_state);
|
||||
if (this->display_switch_ != nullptr)
|
||||
this->display_switch_->publish_state(this->settings_.display_state);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (this->vertical_direction_select_ != nullptr)
|
||||
this->vertical_direction_select_->publish_state(static_cast<size_t>(this->settings_.vertical_direction));
|
||||
#endif
|
||||
}
|
||||
|
||||
void AirtonClimate::save_settings_() { this->settings_pref_.save(&this->settings_); }
|
||||
|
||||
void AirtonClimate::sync_swing_mode_() {
|
||||
this->swing_mode = this->settings_.vertical_direction == VerticalDirection::VERTICAL_DIRECTION_SWING
|
||||
? climate::CLIMATE_SWING_VERTICAL
|
||||
: climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
|
||||
// The unit has no sleep mode while it only fans or picks the mode itself
|
||||
bool AirtonClimate::sleep_mode_allowed_() const {
|
||||
return this->mode != climate::CLIMATE_MODE_FAN_ONLY && this->mode != climate::CLIMATE_MODE_HEAT_COOL;
|
||||
}
|
||||
|
||||
// Each setter stores and saves before it publishes: an automation on the entity runs inside
|
||||
// publish_state() and may set the value again, which must not be undone afterwards
|
||||
bool AirtonClimate::set_sleep_mode_state(bool state) {
|
||||
state = state && this->sleep_mode_allowed_();
|
||||
const bool changed = state != this->settings_.sleep_state;
|
||||
if (changed) {
|
||||
this->settings_.sleep_state = state;
|
||||
this->save_settings_();
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
if (this->sleep_mode_switch_ != nullptr)
|
||||
this->sleep_mode_switch_->publish_state(state);
|
||||
#endif
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool AirtonClimate::set_display_state(bool state) {
|
||||
const bool changed = state != this->settings_.display_state;
|
||||
if (changed) {
|
||||
this->settings_.display_state = state;
|
||||
this->save_settings_();
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
if (this->display_switch_ != nullptr)
|
||||
this->display_switch_->publish_state(state);
|
||||
#endif
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool AirtonClimate::set_vertical_direction_state(VerticalDirection direction) {
|
||||
const bool changed = direction != this->settings_.vertical_direction;
|
||||
if (changed) {
|
||||
this->settings_.vertical_direction = direction;
|
||||
this->sync_swing_mode_();
|
||||
this->save_settings_();
|
||||
}
|
||||
#ifdef USE_SELECT
|
||||
if (this->vertical_direction_select_ != nullptr)
|
||||
this->vertical_direction_select_->publish_state(static_cast<size_t>(direction));
|
||||
#endif
|
||||
return changed;
|
||||
}
|
||||
|
||||
void AirtonClimate::send_settings() {
|
||||
this->transmit_state();
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void AirtonClimate::control(const climate::ClimateCall &call) {
|
||||
// ClimateIR::control() sends and publishes the whole state afterwards
|
||||
// Only a change of swing mode moves the flap, so a repeated swing-off keeps a fixed position
|
||||
if (auto swing_mode = call.get_swing_mode(); swing_mode.has_value() && *swing_mode != this->swing_mode) {
|
||||
this->set_vertical_direction_state(*swing_mode == climate::CLIMATE_SWING_VERTICAL
|
||||
? VerticalDirection::VERTICAL_DIRECTION_SWING
|
||||
: VerticalDirection::VERTICAL_DIRECTION_OFF);
|
||||
}
|
||||
ClimateIR::control(call);
|
||||
}
|
||||
|
||||
void AirtonClimate::transmit_state() {
|
||||
// Entering fan-only or automatic mode drops sleep mode, as the remote does
|
||||
if (!this->sleep_mode_allowed_())
|
||||
this->set_sleep_mode_state(false);
|
||||
// Sampled valid state
|
||||
// Power: On, Mode: 2 (Dry), Fan: 1 (Quiet), Temp: 20C, Swing(V): On, Econo: Off, Turbo: Off, Light: On, Health: On,
|
||||
// Sleep: Off. 0x74C461041A11D3
|
||||
// The turbo bit above the fan speed stays clear, as the remote sends it
|
||||
uint8_t remote_state[AIRTON_STATE_FRAME_SIZE] = {
|
||||
AIRTON_HEADER_BYTE0,
|
||||
AIRTON_HEADER_BYTE1,
|
||||
static_cast<uint8_t>(this->operation_mode_() | (this->fan_speed_() << 4)),
|
||||
this->temperature_(),
|
||||
static_cast<uint8_t>(this->settings_.vertical_direction),
|
||||
this->operation_settings_(),
|
||||
0,
|
||||
};
|
||||
remote_state[6] = checksum(remote_state);
|
||||
|
||||
ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X", remote_state[6], remote_state[5], remote_state[4],
|
||||
remote_state[3], remote_state[2], remote_state[1], remote_state[0]);
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
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->mark(AIRTON_BIT_MARK);
|
||||
data->space(AIRTON_MESSAGE_SPACE);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
uint8_t AirtonClimate::operation_mode_() {
|
||||
uint8_t operating_mode = 0b1000; // First bit is for power state
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
operating_mode |= AIRTON_MODE_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
operating_mode |= AIRTON_MODE_DRY;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
operating_mode |= AIRTON_MODE_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
operating_mode |= AIRTON_MODE_AUTO;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
operating_mode |= AIRTON_MODE_FAN;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
operating_mode = 0b0111 & this->previous_mode_; // Set previous mode with power state bit off
|
||||
}
|
||||
this->previous_mode_ = operating_mode;
|
||||
return operating_mode;
|
||||
}
|
||||
|
||||
uint8_t AirtonClimate::fan_speed_() {
|
||||
uint8_t fan_speed;
|
||||
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
fan_speed = AIRTON_FAN_1;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
fan_speed = AIRTON_FAN_3;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
fan_speed = AIRTON_FAN_5;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
fan_speed = AIRTON_FAN_AUTO;
|
||||
}
|
||||
return fan_speed;
|
||||
}
|
||||
|
||||
uint8_t AirtonClimate::temperature_() {
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
return AIRTON_AUTO_MODE_TEMP - AIRTON_TEMP_MIN;
|
||||
default:
|
||||
return static_cast<uint8_t>(roundf(clamp<float>(this->target_temperature, AIRTON_TEMP_MIN, AIRTON_TEMP_MAX))) -
|
||||
AIRTON_TEMP_MIN;
|
||||
}
|
||||
}
|
||||
|
||||
// The bits of this packet's byte have the following meanings (from MSB to LSB)
|
||||
// Light, Health, Unknown, HeatOn, Unknown, NotAutoOn, Sleep, Econo
|
||||
uint8_t AirtonClimate::operation_settings_() {
|
||||
uint8_t settings = 0;
|
||||
if (this->mode == climate::CLIMATE_MODE_HEAT) { // Set heating bit if on the corresponding mode
|
||||
settings |= (1 << 4);
|
||||
}
|
||||
if (this->settings_.display_state) {
|
||||
settings |= (1 << 7);
|
||||
}
|
||||
if (this->settings_.sleep_state) {
|
||||
settings |= (1 << 1);
|
||||
}
|
||||
settings |= 0b01000000; // Health, as the remote always sends it
|
||||
if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) {
|
||||
settings |= 0b00000100; // NotAutoOn: clear only while the unit picks the mode itself
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
void AirtonClimate::parse_state_frame_(const uint8_t *frame) {
|
||||
uint8_t mode = frame[2];
|
||||
// A later "off" repeats the mode the remote last sent, as the remote itself does
|
||||
this->previous_mode_ = mode & 0b00001111;
|
||||
if (mode & 0b00001000) { // The power bit
|
||||
switch (mode & 0b00000111) {
|
||||
case AIRTON_MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
case AIRTON_MODE_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case AIRTON_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
case AIRTON_MODE_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
case AIRTON_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
uint8_t fan_mode = (frame[2] & 0b01110000) >> 4;
|
||||
|
||||
switch (fan_mode) {
|
||||
case AIRTON_FAN_1:
|
||||
case AIRTON_FAN_2:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case AIRTON_FAN_3:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
case AIRTON_FAN_4:
|
||||
case AIRTON_FAN_5:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
case AIRTON_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
|
||||
this->target_temperature = (frame[3] & 0b00001111) + AIRTON_TEMP_MIN;
|
||||
|
||||
const uint8_t direction = frame[4] & 0b00001111;
|
||||
if (direction <= static_cast<uint8_t>(VerticalDirection::VERTICAL_DIRECTION_DOWN)) {
|
||||
this->set_vertical_direction_state(static_cast<VerticalDirection>(direction));
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Unknown vertical direction %u", direction);
|
||||
}
|
||||
this->set_display_state((frame[5] & 0b10000000) != 0);
|
||||
this->set_sleep_mode_state((frame[5] & 0b00000010) != 0);
|
||||
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
bool AirtonClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
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 (remote_state[0] != AIRTON_HEADER_BYTE0 || remote_state[1] != AIRTON_HEADER_BYTE1) {
|
||||
ESP_LOGV(TAG, "Wrong header contents: %02X %02X", remote_state[1], remote_state[0]);
|
||||
return false;
|
||||
}
|
||||
const uint8_t expected = checksum(remote_state);
|
||||
if (remote_state[AIRTON_STATE_FRAME_SIZE - 1] != expected) {
|
||||
ESP_LOGV(TAG, "Checksum error: calculated %02X, received %02X", expected,
|
||||
remote_state[AIRTON_STATE_FRAME_SIZE - 1]);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X", remote_state[6], remote_state[5], remote_state[4],
|
||||
remote_state[3], remote_state[2], remote_state[1], remote_state[0]);
|
||||
this->parse_state_frame_(remote_state);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::airton
|
||||
@@ -1,137 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::airton {
|
||||
|
||||
// Values for Airton SMVH09B-2A2A3NH IR Controllers
|
||||
static constexpr uint8_t AIRTON_TEMP_MIN = 16; // Celsius
|
||||
static constexpr uint8_t AIRTON_TEMP_MAX = 31; // Celsius
|
||||
static constexpr uint8_t AIRTON_AUTO_MODE_TEMP = 25; // The fixed setpoint the unit uses in auto mode
|
||||
|
||||
static constexpr uint8_t AIRTON_MODE_AUTO = 0b000;
|
||||
static constexpr uint8_t AIRTON_MODE_COOL = 0b001;
|
||||
static constexpr uint8_t AIRTON_MODE_HEAT = 0b100;
|
||||
static constexpr uint8_t AIRTON_MODE_DRY = 0b010;
|
||||
static constexpr uint8_t AIRTON_MODE_FAN = 0b011;
|
||||
|
||||
static constexpr uint8_t AIRTON_FAN_AUTO = 0b000;
|
||||
static constexpr uint8_t AIRTON_FAN_1 = 0b001;
|
||||
static constexpr uint8_t AIRTON_FAN_2 = 0b010;
|
||||
static constexpr uint8_t AIRTON_FAN_3 = 0b011;
|
||||
static constexpr uint8_t AIRTON_FAN_4 = 0b100;
|
||||
static constexpr uint8_t AIRTON_FAN_5 = 0b101;
|
||||
|
||||
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 uint32_t AIRTON_MESSAGE_SPACE = 100000;
|
||||
|
||||
static constexpr uint8_t AIRTON_STATE_FRAME_SIZE = 7;
|
||||
// Every frame starts with these two bytes
|
||||
static constexpr uint8_t AIRTON_HEADER_BYTE0 = 0xD3;
|
||||
static constexpr uint8_t AIRTON_HEADER_BYTE1 = 0x11;
|
||||
|
||||
// Position of the air outlet flap; the values are the nibble the frame carries and the index
|
||||
// of the option in the vertical direction select
|
||||
enum class VerticalDirection : uint8_t {
|
||||
VERTICAL_DIRECTION_OFF = 0,
|
||||
VERTICAL_DIRECTION_SWING = 1,
|
||||
VERTICAL_DIRECTION_UP = 2,
|
||||
VERTICAL_DIRECTION_MIDDLE_UP = 3,
|
||||
VERTICAL_DIRECTION_MIDDLE = 4,
|
||||
VERTICAL_DIRECTION_MIDDLE_DOWN = 5,
|
||||
VERTICAL_DIRECTION_DOWN = 6,
|
||||
};
|
||||
|
||||
// The on/off settings the switch platform exposes
|
||||
enum class AirtonSetting : uint8_t {
|
||||
AIRTON_SETTING_SLEEP_MODE,
|
||||
AIRTON_SETTING_DISPLAY,
|
||||
};
|
||||
|
||||
// The settings the unit keeps that the climate state does not carry; saved to flash when they change
|
||||
struct AirtonSettings {
|
||||
bool sleep_state;
|
||||
bool display_state;
|
||||
VerticalDirection vertical_direction;
|
||||
};
|
||||
|
||||
class AirtonClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
AirtonClimate()
|
||||
: climate_ir::ClimateIR(AIRTON_TEMP_MIN, AIRTON_TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}
|
||||
void setup() override;
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
|
||||
// The direction the settings start from until a saved one is restored in setup()
|
||||
void set_default_vertical_direction(VerticalDirection direction) { this->settings_.vertical_direction = direction; }
|
||||
|
||||
// The setters update the state, publish their entity and save; they return whether anything changed.
|
||||
// A change made here (switch, select, action) is then sent with send_settings(); a frame from the
|
||||
// remote only reports what the unit already did.
|
||||
bool set_sleep_mode_state(bool state);
|
||||
bool set_display_state(bool state);
|
||||
bool set_vertical_direction_state(VerticalDirection direction);
|
||||
void send_settings();
|
||||
void set_setting(AirtonSetting setting, bool state) {
|
||||
const bool changed = setting == AirtonSetting::AIRTON_SETTING_DISPLAY ? this->set_display_state(state)
|
||||
: this->set_sleep_mode_state(state);
|
||||
if (changed)
|
||||
this->send_settings();
|
||||
}
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
void set_sleep_mode_switch(switch_::Switch *sw) { this->sleep_mode_switch_ = sw; }
|
||||
void set_display_switch(switch_::Switch *sw) { this->display_switch_ = sw; }
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void set_vertical_direction_select(select::Select *sel) { this->vertical_direction_select_ = sel; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void publish_settings_();
|
||||
void save_settings_();
|
||||
void sync_swing_mode_();
|
||||
bool sleep_mode_allowed_() const;
|
||||
|
||||
void transmit_state() override;
|
||||
uint8_t operation_mode_();
|
||||
uint8_t fan_speed_();
|
||||
uint8_t temperature_();
|
||||
uint8_t operation_settings_();
|
||||
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
void parse_state_frame_(const uint8_t *frame);
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
switch_::Switch *sleep_mode_switch_{nullptr};
|
||||
switch_::Switch *display_switch_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
select::Select *vertical_direction_select_{nullptr};
|
||||
#endif
|
||||
ESPPreferenceObject settings_pref_;
|
||||
// The unit's own defaults: display lit, no sleep mode, flap still
|
||||
AirtonSettings settings_{false, true, VerticalDirection::VERTICAL_DIRECTION_OFF};
|
||||
// The last mode sent, so turning off repeats it with the power bit cleared
|
||||
uint8_t previous_mode_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::airton
|
||||
@@ -1,70 +0,0 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
CODEOWNERS = ["@Procsiab"]
|
||||
|
||||
airton_ns = cg.esphome_ns.namespace("airton")
|
||||
AirtonClimate = airton_ns.class_("AirtonClimate", climate_ir.ClimateIR)
|
||||
|
||||
CONF_AIRTON_ID = "airton_id"
|
||||
CONF_SLEEP_MODE = "sleep_mode"
|
||||
CONF_VERTICAL_DIRECTION = "vertical_direction"
|
||||
|
||||
VerticalDirection = airton_ns.enum("VerticalDirection", is_class=True)
|
||||
# In the order of the enum, so the option index is the direction value
|
||||
VERTICAL_DIRECTIONS = {
|
||||
"off": VerticalDirection.VERTICAL_DIRECTION_OFF,
|
||||
"swing": VerticalDirection.VERTICAL_DIRECTION_SWING,
|
||||
"up": VerticalDirection.VERTICAL_DIRECTION_UP,
|
||||
"middle-up": VerticalDirection.VERTICAL_DIRECTION_MIDDLE_UP,
|
||||
"middle": VerticalDirection.VERTICAL_DIRECTION_MIDDLE,
|
||||
"middle-down": VerticalDirection.VERTICAL_DIRECTION_MIDDLE_DOWN,
|
||||
"down": VerticalDirection.VERTICAL_DIRECTION_DOWN,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(AirtonClimate).extend(
|
||||
{
|
||||
cv.Optional(CONF_VERTICAL_DIRECTION, default="off"): cv.enum(
|
||||
VERTICAL_DIRECTIONS
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
AIRTON_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(AirtonClimate),
|
||||
}
|
||||
)
|
||||
|
||||
for _name, _call in (
|
||||
(
|
||||
"climate.airton.display_on",
|
||||
"set_setting(esphome::airton::AirtonSetting::AIRTON_SETTING_DISPLAY, true)",
|
||||
),
|
||||
(
|
||||
"climate.airton.display_off",
|
||||
"set_setting(esphome::airton::AirtonSetting::AIRTON_SETTING_DISPLAY, false)",
|
||||
),
|
||||
(
|
||||
"climate.airton.sleep_on",
|
||||
"set_setting(esphome::airton::AirtonSetting::AIRTON_SETTING_SLEEP_MODE, true)",
|
||||
),
|
||||
(
|
||||
"climate.airton.sleep_off",
|
||||
"set_setting(esphome::airton::AirtonSetting::AIRTON_SETTING_SLEEP_MODE, false)",
|
||||
),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, AIRTON_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
# The default until a saved setting is restored in setup(); off is the member's initial value
|
||||
if (direction := config[CONF_VERTICAL_DIRECTION]) != "off":
|
||||
cg.add(var.set_default_vertical_direction(direction))
|
||||
@@ -1,37 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..climate import (
|
||||
CONF_AIRTON_ID,
|
||||
CONF_VERTICAL_DIRECTION,
|
||||
VERTICAL_DIRECTIONS,
|
||||
AirtonClimate,
|
||||
airton_ns,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@Procsiab"]
|
||||
|
||||
VerticalDirectionSelect = airton_ns.class_("VerticalDirectionSelect", select.Select)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_AIRTON_ID): cv.use_id(AirtonClimate),
|
||||
cv.Optional(CONF_VERTICAL_DIRECTION): select.select_schema(
|
||||
VerticalDirectionSelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_AIRTON_ID])
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
await selects(
|
||||
CONF_VERTICAL_DIRECTION,
|
||||
hub.set_vertical_direction_select,
|
||||
options=list(VERTICAL_DIRECTIONS),
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
#include "../airton.h"
|
||||
|
||||
namespace esphome::airton {
|
||||
|
||||
class VerticalDirectionSelect : public select::Select, public Parented<AirtonClimate> {
|
||||
protected:
|
||||
// The options are in the order of the enum, so the index is the direction; the parent publishes the state
|
||||
void control(size_t index) override {
|
||||
if (this->parent_->set_vertical_direction_state(static_cast<VerticalDirection>(index)))
|
||||
this->parent_->send_settings();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::airton
|
||||
@@ -1,41 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DISPLAY, ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
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,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
default_restore_mode="DISABLED",
|
||||
),
|
||||
cv.Optional(CONF_SLEEP_MODE): switch.switch_schema(
|
||||
AirtonSwitch,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
default_restore_mode="DISABLED",
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
await switches(
|
||||
CONF_SLEEP_MODE,
|
||||
hub.set_sleep_mode_switch,
|
||||
AirtonSetting.AIRTON_SETTING_SLEEP_MODE,
|
||||
)
|
||||
@@ -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
|
||||
@@ -42,18 +42,21 @@ bool AM2315C::reset_register_(uint8_t reg) {
|
||||
data[2] = 0;
|
||||
ESP_LOGD(TAG, "Reset register: 0x%02x", reg);
|
||||
if (this->write(data, 3) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
ESP_LOGE(TAG, "Write failed!");
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
delay(5);
|
||||
if (this->read(data, 3) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
ESP_LOGE(TAG, "Read failed!");
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
delay(10);
|
||||
data[0] = 0xB0 | reg;
|
||||
if (this->write(data, 3) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
ESP_LOGE(TAG, "Write failed!");
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
delay(5);
|
||||
@@ -64,7 +67,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
|
||||
uint32_t raw;
|
||||
raw = (data[1] << 12) | (data[2] << 4) | (data[3] >> 4);
|
||||
humidity = raw * 9.5367431640625e-5;
|
||||
raw = encode_uint24(data[3] & 0x0F, data[4], data[5]);
|
||||
raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
|
||||
temperature = raw * 1.9073486328125e-4 - 50;
|
||||
return crc8(data, 6, 0xFF, 0x31, true) == data[6];
|
||||
}
|
||||
@@ -73,7 +76,8 @@ void AM2315C::setup() {
|
||||
// get status
|
||||
uint8_t status = 0;
|
||||
if (this->read(&status, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
ESP_LOGE(TAG, "Read failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,9 +86,18 @@ void AM2315C::setup() {
|
||||
// never required during testing
|
||||
if ((status & 0x18) != 0x18) {
|
||||
ESP_LOGD(TAG, "Resetting AM2315C registers");
|
||||
// reset_register_() marks the component failed with its own reason
|
||||
if (!this->reset_register_(0x1B) || !this->reset_register_(0x1C) || !this->reset_register_(0x1E))
|
||||
if (!this->reset_register_(0x1B)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (!this->reset_register_(0x1C)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (!this->reset_register_(0x1E)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +168,9 @@ void AM2315C::update() {
|
||||
void AM2315C::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AM2315C:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -41,7 +42,9 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -37,13 +37,16 @@ void AM2320Component::setup() {
|
||||
data[0] = 0;
|
||||
data[1] = 4;
|
||||
if (!this->read_data_(data)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
void AM2320Component::dump_config() {
|
||||
ESP_LOGD(TAG, "AM2320:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -43,7 +44,9 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
|
||||
cover.cover_schema(Am43Component)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_PIN, default=8888): cv.uint16_t,
|
||||
cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF),
|
||||
cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
namespace esphome::animation {
|
||||
|
||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||
image::ImageType type, image::Transparency transparent, bool big_endian)
|
||||
: Image(data_start, width, height, type, transparent, big_endian),
|
||||
image::ImageType type, image::Transparency transparent)
|
||||
: Image(data_start, width, height, type, transparent),
|
||||
animation_data_start_(data_start),
|
||||
current_frame_(0),
|
||||
animation_frame_count_(animation_frame_count),
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::animation {
|
||||
class Animation final : public image::Image {
|
||||
public:
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
|
||||
image::Transparency transparent, bool big_endian = false);
|
||||
image::Transparency transparent);
|
||||
|
||||
uint32_t get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_BYTE_ORDER, CONF_LOOP
|
||||
from esphome.components.const import CONF_LOOP
|
||||
from esphome.components.file import image as file_image
|
||||
from esphome.components.file.image import image_schema, write_image
|
||||
from esphome.components.image import Image_, validate_settings
|
||||
@@ -90,7 +90,6 @@ async def setup_animation(config: ConfigType) -> None:
|
||||
frame_count,
|
||||
image_type,
|
||||
trans_value,
|
||||
config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN",
|
||||
)
|
||||
if loop_config := config.get(CONF_LOOP):
|
||||
start = loop_config[CONF_START_FRAME]
|
||||
|
||||
@@ -28,6 +28,15 @@ static constexpr uint32_t APC1_NO_DATA_TIMEOUT_MS = 15000;
|
||||
// Drop a partial frame when the bytes stop mid-way
|
||||
static constexpr uint32_t APC1_RX_IDLE_TIMEOUT_MS = 200;
|
||||
|
||||
// Frames and commands end with the 16-bit sum of every byte before it
|
||||
static uint16_t apc1_checksum(const uint8_t *data, size_t len) {
|
||||
uint16_t sum = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void APC1Component::setup() {
|
||||
if (this->set_pin_ != nullptr) {
|
||||
this->set_pin_->setup();
|
||||
@@ -148,7 +157,7 @@ void APC1Component::loop() {
|
||||
|
||||
void APC1Component::parse_frame_(uint16_t total_len) {
|
||||
uint16_t expected_checksum = encode_uint16(this->rx_buffer_[total_len - 2], this->rx_buffer_[total_len - 1]);
|
||||
uint16_t calculated_checksum = sum16_bytes(this->rx_buffer_.data(), total_len - 2);
|
||||
uint16_t calculated_checksum = apc1_checksum(this->rx_buffer_.data(), total_len - 2);
|
||||
if (expected_checksum != calculated_checksum) {
|
||||
ESP_LOGW(TAG, "APC1 checksum mismatch: calculated 0x%04X != expected 0x%04X", calculated_checksum,
|
||||
expected_checksum);
|
||||
@@ -334,7 +343,7 @@ void APC1Component::send_command_(APC1Command cmd, uint16_t data) {
|
||||
0,
|
||||
0,
|
||||
};
|
||||
uint16_t checksum = sum16_bytes(buf.data(), 5);
|
||||
uint16_t checksum = apc1_checksum(buf.data(), 5);
|
||||
buf[5] = static_cast<uint8_t>((checksum >> 8) & 0xFF);
|
||||
buf[6] = static_cast<uint8_t>(checksum & 0xFF);
|
||||
this->write_array(buf);
|
||||
|
||||
@@ -7,6 +7,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ECO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_PM_0_3UM,
|
||||
CONF_PM_0_5UM,
|
||||
CONF_PM_1_0,
|
||||
@@ -164,7 +165,9 @@ FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await uart.new_uart_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_PM_1_0, var.set_pm_1_0_sensor)
|
||||
@@ -191,8 +194,13 @@ async def to_code(config: ConfigType) -> None:
|
||||
await sensors(CONF_RS3, var.set_rs3_sensor)
|
||||
await sensors(CONF_ERROR_CODE, var.set_error_code_sensor)
|
||||
|
||||
await cg.add_pin(var.set_set_pin, config.get(CONF_SET_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
if (set_pin_config := config.get(CONF_SET_PIN)) is not None:
|
||||
set_pin = await cg.gpio_pin_expression(set_pin_config)
|
||||
cg.add(var.set_set_pin(set_pin))
|
||||
|
||||
if (reset_pin_config := config.get(CONF_RESET_PIN)) is not None:
|
||||
reset_pin = await cg.gpio_pin_expression(reset_pin_config)
|
||||
cg.add(var.set_reset_pin(reset_pin))
|
||||
|
||||
|
||||
APC1_ACTION_SCHEMA = maybe_simple_id(
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "apc_proteous"
|
||||
@@ -1,236 +0,0 @@
|
||||
#include "apc_proteous_cover.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::apc_proteous {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "apc_proteous.cover");
|
||||
static constexpr const char *START_CMD = "*1\r"; // start/stop toggle
|
||||
static constexpr const char *OPEN_CMD = "*6\r";
|
||||
static constexpr const char *CLOSE_CMD = "*7\r";
|
||||
static constexpr const char *QUERY_S = "?s\r";
|
||||
static constexpr const char *QUERY_X = "?x\r";
|
||||
|
||||
// Depending on calibration the reported position may stop a little short of the physical limits
|
||||
// (e.g. 99% at the fully open reed switch), so readings this close to an end count as that end.
|
||||
static constexpr uint8_t ENDPOINT_MARGIN = 2;
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void APCProteousCover::setup() {
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
restore->apply(this);
|
||||
} else {
|
||||
this->position = 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
void APCProteousCover::loop() {
|
||||
uint8_t data;
|
||||
while (this->available() > 0) {
|
||||
if (!this->read_byte(&data)) {
|
||||
continue;
|
||||
}
|
||||
const char *line = this->line_reader_.feed(data);
|
||||
if (this->line_reader_.overflowed()) {
|
||||
// Start over from this byte, so a frame right after unterminated noise is still read
|
||||
ESP_LOGVV(TAG, "rx overflow, discarding");
|
||||
this->line_reader_.restart_with(data);
|
||||
} else if (line != nullptr && line[0] != '\0') {
|
||||
ESP_LOGVV(TAG, "rx: '%s'", line);
|
||||
this->parse_response_(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APCProteousCover::parse_response_(const char *line) {
|
||||
// Expected format: "?s=XX" or "?x=XX" where XX is a hex value
|
||||
if (line[0] != '?' || line[1] == '\0' || line[2] != '=' || line[3] == '\0') {
|
||||
ESP_LOGV(TAG, "Invalid response: %s", line);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *hex_str = line + 3;
|
||||
char *end_ptr;
|
||||
uint8_t value = static_cast<uint8_t>(strtol(hex_str, &end_ptr, 16));
|
||||
if (end_ptr == hex_str) {
|
||||
ESP_LOGW(TAG, "Failed to parse hex value: %s", line);
|
||||
return;
|
||||
}
|
||||
|
||||
bool state_changed = false;
|
||||
char type = line[1];
|
||||
if (type == 's') {
|
||||
// bit 0 = operating, bit 1 = direction (0 = opening, 1 = closing)
|
||||
CoverOperation new_operation = COVER_OPERATION_IDLE;
|
||||
if (value & 0x01) {
|
||||
new_operation = (value & 0x02) ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
|
||||
}
|
||||
if (this->current_operation != new_operation) {
|
||||
this->current_operation = new_operation;
|
||||
state_changed = true;
|
||||
}
|
||||
// Motion in the commanded direction acknowledges the pending command
|
||||
if ((this->pending_command_ == OPEN_CMD && new_operation == COVER_OPERATION_OPENING) ||
|
||||
(this->pending_command_ == CLOSE_CMD && new_operation == COVER_OPERATION_CLOSING)) {
|
||||
this->clear_pending_();
|
||||
}
|
||||
// A partial move is over once the acknowledged motion has stopped, wherever it stopped, so an
|
||||
// old target cannot halt a movement started from the wall button or the remote later on
|
||||
if (new_operation == COVER_OPERATION_IDLE && this->pending_command_ == nullptr) {
|
||||
this->target_position_.reset();
|
||||
}
|
||||
ESP_LOGV(TAG, "s-status: 0x%02X (operation=%d)", value, new_operation);
|
||||
} else if (type == 'x') {
|
||||
// Position percentage, snapped to the ends so is_open/is_closed can latch despite calibration offsets
|
||||
float new_position;
|
||||
if (value + ENDPOINT_MARGIN >= 100) {
|
||||
new_position = COVER_OPEN;
|
||||
} else if (value <= ENDPOINT_MARGIN) {
|
||||
new_position = COVER_CLOSED;
|
||||
} else {
|
||||
new_position = value / 100.0f;
|
||||
}
|
||||
if (this->position != new_position) {
|
||||
this->position = new_position;
|
||||
state_changed = true;
|
||||
// Until the controller has confirmed motion in the commanded direction, current_operation may
|
||||
// still show the previous one, so a reversing partial move must not be judged against it
|
||||
if (this->target_position_.has_value() && this->pending_command_ == nullptr &&
|
||||
((this->current_operation == COVER_OPERATION_OPENING && this->position >= *this->target_position_) ||
|
||||
(this->current_operation == COVER_OPERATION_CLOSING && this->position <= *this->target_position_))) {
|
||||
ESP_LOGD(TAG, "Target position %.2f reached", *this->target_position_);
|
||||
this->stop_cmd_(App.get_loop_component_start_time());
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "x-status: %d%% (position=%.2f)", value, this->position);
|
||||
}
|
||||
|
||||
if (state_changed) {
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
void APCProteousCover::clear_pending_() {
|
||||
this->pending_command_ = nullptr;
|
||||
this->command_retries_ = 0;
|
||||
}
|
||||
|
||||
void APCProteousCover::retry_pending_command_(uint32_t now) {
|
||||
if (this->pending_command_ == nullptr || (now - this->pending_command_time_) < COMMAND_ACK_MS) {
|
||||
return;
|
||||
}
|
||||
bool is_open_cmd = this->pending_command_ == OPEN_CMD;
|
||||
// Nothing to resend once the gate is at the commanded end
|
||||
if ((is_open_cmd && this->position >= COVER_OPEN) || (!is_open_cmd && this->position <= COVER_CLOSED)) {
|
||||
this->clear_pending_();
|
||||
return;
|
||||
}
|
||||
const LogString *label = is_open_cmd ? LOG_STR("open") : LOG_STR("close");
|
||||
if (this->command_retries_ >= MAX_COMMAND_RETRIES) {
|
||||
ESP_LOGW(TAG, "Gate did not acknowledge %s command after %u retries", LOG_STR_ARG(label), this->command_retries_);
|
||||
this->clear_pending_();
|
||||
this->target_position_.reset();
|
||||
return;
|
||||
}
|
||||
this->command_retries_++;
|
||||
ESP_LOGD(TAG, "Gate did not respond, resending %s command (retry %u)", LOG_STR_ARG(label), this->command_retries_);
|
||||
this->pending_command_time_ = now;
|
||||
this->write_command_(this->pending_command_, now);
|
||||
}
|
||||
|
||||
void APCProteousCover::update() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->retry_pending_command_(now);
|
||||
|
||||
// Let the command and its echo clear the line before the next query
|
||||
if ((now - this->last_command_tx_) < COMMAND_QUIET_MS) {
|
||||
return;
|
||||
}
|
||||
// The line is idle, so anything left in the buffer is an unterminated echo
|
||||
if (this->line_reader_.length() != 0) {
|
||||
ESP_LOGVV(TAG, "discarding stale rx '%s' before query", this->line_reader_.partial());
|
||||
}
|
||||
this->line_reader_.reset();
|
||||
|
||||
const char *query = this->query_s_next_ ? QUERY_S : QUERY_X;
|
||||
ESP_LOGVV(TAG, "tx: %s", query);
|
||||
this->write_str(query);
|
||||
this->query_s_next_ = !this->query_s_next_;
|
||||
}
|
||||
|
||||
void APCProteousCover::dump_config() {
|
||||
LOG_COVER("", "APC Proteous Cover", this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
CoverTraits APCProteousCover::get_traits() {
|
||||
auto traits = CoverTraits();
|
||||
traits.set_supports_position(true);
|
||||
traits.set_supports_stop(true);
|
||||
traits.set_supports_toggle(true);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void APCProteousCover::write_command_(const char *cmd, uint32_t now) {
|
||||
ESP_LOGVV(TAG, "tx: %s", cmd);
|
||||
this->last_command_tx_ = now;
|
||||
this->write_str(cmd);
|
||||
}
|
||||
|
||||
void APCProteousCover::send_command_(const char *cmd) {
|
||||
// The published state comes only from status reads; this just records the command so a stop
|
||||
// can cancel it before the controller reports motion, and so it can be resent if dropped.
|
||||
// The commands are idempotent, so only skip one when the gate is already moving that way.
|
||||
CoverOperation operation = cmd == OPEN_CMD ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING;
|
||||
if (this->current_operation == operation) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending %s command",
|
||||
operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("open") : LOG_STR_LITERAL("close"));
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->pending_command_ = cmd;
|
||||
this->pending_command_time_ = now;
|
||||
this->command_retries_ = 0;
|
||||
this->write_command_(cmd, now);
|
||||
}
|
||||
|
||||
void APCProteousCover::stop_cmd_(uint32_t now) {
|
||||
// START_CMD is a start/stop toggle, so it is only sent while the gate is moving, or while a
|
||||
// movement command is in flight that the controller has not reported yet; otherwise it would start the gate.
|
||||
if (this->current_operation == COVER_OPERATION_IDLE && this->pending_command_ == nullptr) {
|
||||
ESP_LOGD(TAG, "Cover already idle, ignoring stop command");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending stop command");
|
||||
this->write_command_(START_CMD, now);
|
||||
this->clear_pending_();
|
||||
this->target_position_.reset();
|
||||
}
|
||||
|
||||
void APCProteousCover::control(const CoverCall &call) {
|
||||
if (call.get_stop()) {
|
||||
this->stop_cmd_(App.get_loop_component_start_time());
|
||||
} else if (call.get_position().has_value()) {
|
||||
float target = *call.get_position();
|
||||
ESP_LOGD(TAG, "Target position: %.2f, current: %.2f", target, this->position);
|
||||
// There is no position command: a partial target is reached by moving towards it and stopping
|
||||
// once the reported position gets there
|
||||
this->target_position_.reset();
|
||||
if (target == COVER_OPEN) {
|
||||
this->send_command_(OPEN_CMD);
|
||||
} else if (target == COVER_CLOSED) {
|
||||
this->send_command_(CLOSE_CMD);
|
||||
} else if (target != this->position) {
|
||||
this->target_position_ = target;
|
||||
this->send_command_(target > this->position ? OPEN_CMD : CLOSE_CMD);
|
||||
}
|
||||
} else if (call.get_toggle()) {
|
||||
this->clear_pending_();
|
||||
this->target_position_.reset();
|
||||
this->write_command_(START_CMD, App.get_loop_component_start_time());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::apc_proteous
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/line_reader.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
namespace esphome::apc_proteous {
|
||||
|
||||
class APCProteousCover : public cover::Cover, public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
cover::CoverTraits get_traits() override;
|
||||
|
||||
protected:
|
||||
void control(const cover::CoverCall &call) override;
|
||||
void parse_response_(const char *line);
|
||||
void send_command_(const char *cmd);
|
||||
void write_command_(const char *cmd, uint32_t now);
|
||||
void stop_cmd_(uint32_t now);
|
||||
void retry_pending_command_(uint32_t now);
|
||||
void clear_pending_();
|
||||
|
||||
// The longest valid frame is "?s=XX"; anything longer is line noise
|
||||
static constexpr size_t MAX_RESPONSE_LEN = 16;
|
||||
|
||||
// A movement command is acknowledged once the controller reports motion in the commanded
|
||||
// direction; resend it this often, up to MAX_COMMAND_RETRIES times, since the serial link
|
||||
// occasionally drops one. The stop toggle is never resent because a spurious toggle could start the gate.
|
||||
static constexpr uint32_t COMMAND_ACK_MS = 2000;
|
||||
static constexpr uint8_t MAX_COMMAND_RETRIES = 3;
|
||||
|
||||
// The controller echoes every command, sometimes without a terminator. A query sent too soon
|
||||
// after a command collides with that echo, so polling pauses for this long after each command.
|
||||
static constexpr uint32_t COMMAND_QUIET_MS = 250;
|
||||
|
||||
// Set only for a partial target; the gate is stopped when the reported position reaches it
|
||||
optional<float> target_position_{};
|
||||
// Last movement command sent and not yet acknowledged; lets a stop cancel it and drives the retries
|
||||
const char *pending_command_{nullptr};
|
||||
uint32_t pending_command_time_{0};
|
||||
uint32_t last_command_tx_{0};
|
||||
LineReader<MAX_RESPONSE_LEN + 1, '\r', '\n'> line_reader_;
|
||||
uint8_t command_retries_{0};
|
||||
bool query_s_next_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::apc_proteous
|
||||
@@ -1,33 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import cover, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
apc_proteous_ns = cg.esphome_ns.namespace("apc_proteous")
|
||||
APCProteousCover = apc_proteous_ns.class_(
|
||||
"APCProteousCover", cover.Cover, cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cover.cover_schema(APCProteousCover)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.polling_component_schema("500ms"))
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
DOMAIN,
|
||||
baud_rate=9600,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await cover.new_cover(config)
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -34,7 +34,8 @@ enum { // APDS9306 registers
|
||||
|
||||
#define APDS9306_ERROR_CHECK(func, error) \
|
||||
if (!(func)) { \
|
||||
this->mark_failed(LOG_STR(error)); \
|
||||
ESP_LOGE(TAG, error); \
|
||||
this->mark_failed(); \
|
||||
return; \
|
||||
}
|
||||
#define APDS9306_WARNING_CHECK(func, warning) \
|
||||
@@ -47,14 +48,23 @@ enum { // APDS9306 registers
|
||||
ESP_LOGV(TAG, "Writing 0x%02x to 0x%02x", value, reg); \
|
||||
if (!this->write_byte(reg, value)) { \
|
||||
ESP_LOGE(TAG, "Failed writing 0x%02x to 0x%02x", value, reg); \
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); \
|
||||
this->mark_failed(); \
|
||||
return; \
|
||||
}
|
||||
|
||||
void APDS9306::setup() {
|
||||
// 0xB1 for APDS9306, 0xB3 for APDS9306-065; they differ only in bit 1
|
||||
if (!this->check_chip_id(this, APDS9306_PART_ID, 0xB1, 0xFD))
|
||||
uint8_t id;
|
||||
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != 0xB1 && id != 0xB3) { // 0xB1 for APDS9306 0xB3 for APDS9306-065
|
||||
this->error_code_ = WRONG_ID;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// ALS resolution and measurement, see datasheet or init.py for options
|
||||
uint8_t als_meas_rate = ((this->bit_width_ & 0x07) << 4) | (this->measurement_rate_ & 0x07);
|
||||
@@ -79,6 +89,20 @@ void APDS9306::dump_config() {
|
||||
LOG_SENSOR("", "APDS9306", this);
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed()) {
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case WRONG_ID:
|
||||
ESP_LOGE(TAG, "APDS9306 has invalid id!");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Setting up APDS9306 registers failed!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Gain: %u\n"
|
||||
" Measurement rate: %u\n"
|
||||
|
||||
@@ -50,6 +50,12 @@ class APDS9306 final : public sensor::Sensor, public PollingComponent, public i2
|
||||
void set_ambient_light_gain(AmbientLightGain gain) { this->gain_ = gain; }
|
||||
|
||||
protected:
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
WRONG_ID,
|
||||
} error_code_{NONE};
|
||||
|
||||
MeasurementBitWidth bit_width_;
|
||||
MeasurementRate measurement_rate_;
|
||||
AmbientLightGain gain_;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "apds9930.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -65,8 +64,16 @@ static constexpr uint16_t ALS_SATURATION = 1024 * (256 - APDS9930_ATIME_VALUE);
|
||||
bool APDS9930Component::write_reg_(uint8_t reg, uint8_t value) { return this->write_byte(reg | APDS9930_CMD, value); }
|
||||
|
||||
void APDS9930Component::setup() {
|
||||
if (!this->check_chip_id(this, APDS9930_ID | APDS9930_CMD, APDS9930_CHIP_ID))
|
||||
uint8_t id;
|
||||
if (!this->read_byte(APDS9930_ID | APDS9930_CMD, &id)) {
|
||||
this->mark_failed(LOG_STR("Communication failed"));
|
||||
return;
|
||||
}
|
||||
if (id != APDS9930_CHIP_ID) {
|
||||
ESP_LOGE(TAG, "Wrong ID 0x%02X (expected 0x%02X)", id, APDS9930_CHIP_ID);
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t control =
|
||||
(this->led_drive_ & 0b11) << CONTROL_PDRIVE_SHIFT | CONTROL_PDIODE_CH1 << CONTROL_PDIODE_SHIFT |
|
||||
@@ -112,8 +119,8 @@ void APDS9930Component::update() {
|
||||
const uint8_t status = raw[0];
|
||||
|
||||
if (this->illuminance_sensor_ != nullptr && (status & APDS9930_AVALID) != 0) {
|
||||
const uint16_t ch0 = encode_value_le<uint16_t>(&raw[1]);
|
||||
const uint16_t ch1 = encode_value_le<uint16_t>(&raw[3]);
|
||||
const uint16_t ch0 = encode_uint16(raw[2], raw[1]);
|
||||
const uint16_t ch1 = encode_uint16(raw[4], raw[3]);
|
||||
if (ch0 >= ALS_SATURATION || ch1 >= ALS_SATURATION) {
|
||||
// A saturated channel would give a lux value that is too low, not too high
|
||||
ESP_LOGW(TAG, "Light channel saturated, lower ambient_light_gain");
|
||||
@@ -126,7 +133,7 @@ void APDS9930Component::update() {
|
||||
}
|
||||
|
||||
if (this->proximity_sensor_ != nullptr && (status & APDS9930_PVALID) != 0) {
|
||||
this->proximity_sensor_->publish_state(encode_value_le<uint16_t>(&raw[5]));
|
||||
this->proximity_sensor_->publish_state(encode_uint16(raw[6], raw[5]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_ILLUMINANCE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
@@ -58,7 +59,9 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_led_drive(config[CONF_LED_DRIVE]))
|
||||
cg.add(var.set_proximity_gain(config[CONF_PROXIMITY_GAIN]))
|
||||
cg.add(var.set_ambient_gain(config[CONF_AMBIENT_LIGHT_GAIN]))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -59,7 +60,9 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_led_drive(config[CONF_LED_DRIVE]))
|
||||
cg.add(var.set_proximity_gain(config[CONF_PROXIMITY_GAIN]))
|
||||
cg.add(var.set_ambient_gain(config[CONF_AMBIENT_LIGHT_GAIN]))
|
||||
|
||||
@@ -8,7 +8,7 @@ ESPHOME_LOG_TAG(TAG, "apds9960");
|
||||
|
||||
#define APDS9960_ERROR_CHECK(func) \
|
||||
if (!(func)) { \
|
||||
this->mark_failed(LOG_STR("Setting up APDS9960 registers failed")); \
|
||||
this->mark_failed(); \
|
||||
return; \
|
||||
}
|
||||
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
|
||||
@@ -16,12 +16,14 @@ ESPHOME_LOG_TAG(TAG, "apds9960");
|
||||
void APDS9960::setup() {
|
||||
uint8_t id;
|
||||
if (!this->read_byte(0x92, &id)) { // ID register
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
|
||||
this->mark_failed(LOG_STR("APDS9960 has invalid id"));
|
||||
this->error_code_ = WRONG_ID;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,6 +135,20 @@ void APDS9960::dump_config() {
|
||||
LOG_SENSOR(" ", "Clear channel", this->clear_sensor_);
|
||||
LOG_SENSOR(" ", "Proximity", this->proximity_sensor_);
|
||||
#endif
|
||||
|
||||
if (this->is_failed()) {
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case WRONG_ID:
|
||||
ESP_LOGE(TAG, "APDS9960 has invalid id!");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Setting up APDS9960 registers failed!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define APDS9960_WARNING_CHECK(func, warning) \
|
||||
|
||||
@@ -58,6 +58,11 @@ class APDS9960 final : public PollingComponent, public i2c::I2CDevice {
|
||||
uint8_t gesture_gain_;
|
||||
uint8_t gesture_wait_time_;
|
||||
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
WRONG_ID,
|
||||
} error_code_{NONE};
|
||||
bool gesture_up_started_{false};
|
||||
bool gesture_down_started_{false};
|
||||
bool gesture_left_started_{false};
|
||||
|
||||
@@ -251,6 +251,9 @@ def _validate_supports_response(value: Any) -> str:
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
|
||||
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
|
||||
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
|
||||
|
||||
VARIABLE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
|
||||
@@ -481,6 +484,29 @@ def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
|
||||
return strings
|
||||
|
||||
|
||||
def _action_strings_size(strings: list[str | None]) -> int:
|
||||
"""Bytes needed to copy every string out of flash, each with its terminator."""
|
||||
return sum(
|
||||
len(string.encode("utf-8")) + 1 for string in strings if string is not None
|
||||
)
|
||||
|
||||
|
||||
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
|
||||
if not CORE.is_esp8266:
|
||||
return config
|
||||
actions = config.get(CONF_ACTIONS, [])
|
||||
has_metadata = _has_action_metadata(actions)
|
||||
for conf in actions:
|
||||
size = _action_strings_size(_action_strings(conf, has_metadata))
|
||||
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
|
||||
raise cv.Invalid(
|
||||
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
|
||||
f"description and example text; ESP8266 allows at most "
|
||||
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
|
||||
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
|
||||
if (
|
||||
@@ -505,6 +531,7 @@ def _validate_wizard(config: ConfigType) -> ConfigType:
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
_validate_esp8266_action_strings,
|
||||
_validate_outgoing_host_ipv6,
|
||||
_validate_wizard,
|
||||
)
|
||||
@@ -560,6 +587,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_HOMEASSISTANT_STATES]:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||
|
||||
scratch_size = 0
|
||||
if actions:
|
||||
# Metadata is compiled in for every action once any action declares it, because the
|
||||
# string table layout is fixed by the define rather than per action
|
||||
@@ -611,6 +639,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
func_args.append((native, name))
|
||||
strings = _action_strings(conf, has_metadata)
|
||||
table = _add_action_strings(index, strings)
|
||||
if CORE.is_esp8266:
|
||||
scratch_size = max(scratch_size, _action_strings_size(strings))
|
||||
# Template args: supports_response mode, then user service arg types
|
||||
templ = cg.TemplateArguments(supports_response, *service_template_args)
|
||||
# Key is hashed here because the name is not readable at runtime on ESP8266
|
||||
@@ -637,6 +667,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(auto.add_action(unregister_action))
|
||||
# Register all services at once - single allocation, no reallocations
|
||||
cg.add(var.initialize_user_services(triggers))
|
||||
if CORE.is_esp8266 and has_user_actions:
|
||||
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
|
||||
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
|
||||
|
||||
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
|
||||
await wizard.to_code(wizard_config)
|
||||
|
||||
@@ -403,9 +403,6 @@ message WizardCapabilities {
|
||||
message SendspinCapabilities {
|
||||
// Bitmask of the Sendspin features this device supports
|
||||
uint32 feature_flags = 1;
|
||||
// The Sendspin client_id: base64url of the client's static public key, 43 characters.
|
||||
// Empty until the Sendspin client has started once since boot.
|
||||
string client_id = 2 [(max_data_length) = 43];
|
||||
}
|
||||
|
||||
message DeviceCapabilitiesResponse {
|
||||
@@ -1029,8 +1026,8 @@ message SubscribeHomeassistantServicesRequest {
|
||||
}
|
||||
|
||||
message HomeassistantServiceMap {
|
||||
string key = 1 [(progmem) = true];
|
||||
string value = 2 [(progmem) = true];
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message HomeassistantActionRequest {
|
||||
@@ -1039,7 +1036,7 @@ message HomeassistantActionRequest {
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||
|
||||
string service = 1 [(progmem) = true];
|
||||
string service = 1;
|
||||
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
|
||||
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
|
||||
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
|
||||
@@ -1160,21 +1157,21 @@ enum SupportsResponseType {
|
||||
}
|
||||
message ListEntitiesServicesArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
string name = 1 [(progmem) = true];
|
||||
string name = 1;
|
||||
ServiceArgType type = 2;
|
||||
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
|
||||
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
|
||||
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
}
|
||||
message ListEntitiesServicesResponse {
|
||||
option (id) = 41;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
|
||||
string name = 1 [(progmem) = true];
|
||||
string name = 1;
|
||||
fixed32 key = 2 [(force) = true];
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
SupportsResponseType supports_response = 4;
|
||||
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
|
||||
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
@@ -3046,15 +3043,12 @@ enum SerialProxyIdentityFlag {
|
||||
// IDs below are empty
|
||||
}
|
||||
|
||||
// The descriptor fields of a USB device as seen by the host stack, and the port's index
|
||||
// among those a vendor driver exposes on the interface
|
||||
// The descriptor fields of a USB device as seen by the host stack
|
||||
message UsbDeviceDescriptor {
|
||||
uint32 vendor_id = 1;
|
||||
uint32 product_id = 2;
|
||||
uint32 bcd_device = 3;
|
||||
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
|
||||
uint32 interface_class = 5; // bInterfaceClass of that interface
|
||||
uint32 port_number = 6;
|
||||
uint32 interface_number = 4; // bInterfaceNumber the host driver binds to
|
||||
}
|
||||
|
||||
// The identity of the device behind a port. Identifies one physical device among others of
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#include "api_connection.h"
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
#ifdef USE_API
|
||||
#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -454,15 +451,6 @@ bool APIConnection::send_disconnect_response_() {
|
||||
DisconnectResponse resp;
|
||||
return this->send_message(resp);
|
||||
}
|
||||
void APIConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
|
||||
logger::log_crash_to_client();
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::on_disconnect_response() {
|
||||
// Don't close socket here, let APIServer::loop() do it
|
||||
// so getpeername() still works for the disconnect trigger
|
||||
@@ -2131,7 +2119,6 @@ bool APIConnection::send_device_capabilities_response_() {
|
||||
#endif
|
||||
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
|
||||
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
|
||||
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
|
||||
#endif
|
||||
return this->send_message(resp);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
#include "subscribe_state.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
#include "esphome/components/esp32/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_RP2_CRASH_HANDLER
|
||||
#include "esphome/components/rp2/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
#include "esphome/components/esp8266/crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
@@ -289,7 +298,21 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg);
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
esp32::crash_handler_log();
|
||||
esp32::crash_handler_clear();
|
||||
#endif
|
||||
#ifdef USE_RP2_CRASH_HANDLER
|
||||
rp2::crash_handler_log();
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
esp8266::crash_handler_log();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; }
|
||||
#endif
|
||||
|
||||
@@ -123,7 +123,7 @@ extend google.protobuf.FieldOptions {
|
||||
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
|
||||
optional bool track_presence = 50020 [default=false];
|
||||
|
||||
// progmem: The string field, or pointer_to_buffer bytes field, points into flash (PROGMEM).
|
||||
// progmem: The pointer_to_buffer bytes field points into flash (PROGMEM).
|
||||
// The generated encode and dump copy it with progmem_memcpy, as ESP8266
|
||||
// cannot read flash with ordinary loads.
|
||||
optional bool progmem = 50021 [default=false];
|
||||
|
||||
@@ -310,14 +310,12 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) {
|
||||
uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.client_id);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
|
||||
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.feature_flags);
|
||||
size += !msg.client_id.empty() ? 2 + msg.client_id.size() : 0;
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
@@ -1142,8 +1140,8 @@ uint32_t NoiseEncryptionSetKeyResponse::calc_size_msg(const void *self) {
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
uint8_t *HomeassistantServiceMap::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const HomeassistantServiceMap *>(self);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
|
||||
return pos;
|
||||
}
|
||||
uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
|
||||
@@ -1155,7 +1153,7 @@ uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
|
||||
}
|
||||
uint8_t *HomeassistantActionRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const HomeassistantActionRequest *>(self);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
|
||||
for (auto &it : msg.data) {
|
||||
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 2, it);
|
||||
}
|
||||
@@ -1326,13 +1324,13 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
|
||||
uint8_t *ListEntitiesServicesArgument::encode_msg(const void *self,
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const ListEntitiesServicesArgument *>(self);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(msg.type));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -1352,14 +1350,14 @@ uint32_t ListEntitiesServicesArgument::calc_size_msg(const void *self) {
|
||||
uint8_t *ListEntitiesServicesResponse::encode_msg(const void *self,
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const ListEntitiesServicesResponse *>(self);
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
|
||||
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, msg.key);
|
||||
for (auto &it : msg.args) {
|
||||
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 3, it);
|
||||
}
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.supports_response));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
|
||||
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -4177,8 +4175,6 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
|
||||
return pos;
|
||||
}
|
||||
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
|
||||
@@ -4188,8 +4184,6 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.product_id);
|
||||
size += ProtoSize::calc_uint32(1, msg.bcd_device);
|
||||
size += ProtoSize::calc_uint32(1, msg.interface_number);
|
||||
size += ProtoSize::calc_uint32(1, msg.interface_class);
|
||||
size += ProtoSize::calc_uint32(1, msg.port_number);
|
||||
return size;
|
||||
}
|
||||
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
|
||||
@@ -750,7 +750,6 @@ class WizardCapabilities final : public ProtoMessage {
|
||||
class SendspinCapabilities final : public ProtoMessage {
|
||||
public:
|
||||
uint32_t feature_flags{0};
|
||||
StringRef client_id{nullptr, 0}; // null until set, encode only
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
@@ -1389,8 +1388,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
class HomeassistantServiceMap final : public ProtoMessage {
|
||||
public:
|
||||
ProgmemStringRef key{};
|
||||
ProgmemStringRef value{};
|
||||
StringRef key{nullptr, 0}; // null until set, encode only
|
||||
StringRef value{nullptr, 0}; // null until set, encode only
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
@@ -1410,7 +1409,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); }
|
||||
#endif
|
||||
ProgmemStringRef service{};
|
||||
StringRef service{nullptr, 0}; // null until set, encode only
|
||||
FixedVector<HomeassistantServiceMap> data{};
|
||||
FixedVector<HomeassistantServiceMap> data_template{};
|
||||
FixedVector<HomeassistantServiceMap> variables{};
|
||||
@@ -1577,13 +1576,13 @@ class GetTimeResponse final : public ProtoDecodableMessage {
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
public:
|
||||
ProgmemStringRef name{};
|
||||
StringRef name{nullptr, 0}; // null until set, encode only
|
||||
enums::ServiceArgType type{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProgmemStringRef description{};
|
||||
StringRef description{nullptr, 0}; // null until set, encode only
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProgmemStringRef example{};
|
||||
StringRef example{nullptr, 0}; // null until set, encode only
|
||||
#endif
|
||||
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 {
|
||||
@@ -1604,12 +1603,12 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
|
||||
#endif
|
||||
ProgmemStringRef name{};
|
||||
StringRef name{nullptr, 0}; // null until set, encode only
|
||||
uint32_t key{0};
|
||||
FixedVector<ListEntitiesServicesArgument> args{};
|
||||
enums::SupportsResponseType supports_response{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProgmemStringRef description{};
|
||||
StringRef description{nullptr, 0}; // null until set, encode only
|
||||
#endif
|
||||
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 {
|
||||
@@ -4084,8 +4083,6 @@ class UsbDeviceDescriptor final : public ProtoMessage {
|
||||
uint32_t product_id{0};
|
||||
uint32_t bcd_device{0};
|
||||
uint32_t interface_number{0};
|
||||
uint32_t interface_class{0};
|
||||
uint32_t port_number{0};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -12,9 +12,8 @@ namespace esphome::api {
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site
|
||||
void DumpBuffer::append_p_esp8266(const char *str) { this->append_p_esp8266(str, strlen_P(str)); }
|
||||
|
||||
void DumpBuffer::append_p_esp8266(const char *str, size_t len) {
|
||||
void DumpBuffer::append_p_esp8266(const char *str) {
|
||||
size_t len = strlen_P(str);
|
||||
size_t space = CAPACITY - 1 - pos_;
|
||||
if (len > space)
|
||||
len = space;
|
||||
@@ -135,13 +134,6 @@ static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, co
|
||||
progmem_memcpy(data_buf, data, len);
|
||||
dump_bytes_field(out, field_name, data_buf, len, indent);
|
||||
}
|
||||
|
||||
// Helper for string fields in flash
|
||||
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const ProgmemStringRef &value,
|
||||
int indent = 2) {
|
||||
append_field_prefix(out, field_name, indent);
|
||||
out.append("'").append_p(value.progmem_ptr(), value.size()).append("'\n");
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
template<> const char *proto_enum_to_string<enums::DisconnectReason>(enums::DisconnectReason value) {
|
||||
@@ -1127,7 +1119,6 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
|
||||
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
|
||||
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
|
||||
dump_field(out, ESPHOME_PSTR("client_id"), this->client_id);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
@@ -1520,13 +1511,13 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap"));
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("value"), this->value);
|
||||
dump_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
dump_field(out, ESPHOME_PSTR("value"), this->value);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest"));
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("service"), this->service);
|
||||
dump_field(out, ESPHOME_PSTR("service"), this->service);
|
||||
for (const auto &it : this->data) {
|
||||
out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": ");
|
||||
it.dump_to(out);
|
||||
@@ -1621,19 +1612,19 @@ const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
dump_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("example"), this->example);
|
||||
dump_field(out, ESPHOME_PSTR("example"), this->example);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse"));
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
for (const auto &it : this->args) {
|
||||
out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": ");
|
||||
@@ -1642,7 +1633,7 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
|
||||
}
|
||||
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
dump_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
@@ -2951,8 +2942,6 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
|
||||
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
|
||||
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
|
||||
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
|
||||
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -65,9 +65,6 @@ void APIServer::socket_failed_(const LogString *msg) {
|
||||
}
|
||||
|
||||
void APIServer::setup() {
|
||||
#if defined(USE_API_HOMEASSISTANT_STATES) && defined(HOMEASSISTANT_STATE_SUBSCRIPTION_COUNT)
|
||||
this->state_subs_.reserve(HOMEASSISTANT_STATE_SUBSCRIPTION_COUNT);
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
// Always reserve the slot: flash preferences are positional on esp8266, so
|
||||
// a yaml key build must keep the layout of a runtime key build
|
||||
@@ -550,7 +547,7 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
||||
// arrive before the subscription and are lost - warn instead of failing silently.
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
|
||||
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
|
||||
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.progmem_ptr(),
|
||||
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
|
||||
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
|
||||
: LOG_STR_LITERAL("no client connected"));
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.service = ProgmemStringRef(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -267,12 +267,12 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.service = ProgmemStringRef(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.key = ProgmemStringRef(StringRef(it.first));
|
||||
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
|
||||
kv.key = StringRef(it.first);
|
||||
kv.value = StringRef(it.second); // data map lives until send completes
|
||||
}
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
@@ -289,7 +289,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &event_name) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.service = ProgmemStringRef(StringRef(event_name));
|
||||
resp.service = StringRef(event_name);
|
||||
resp.is_event = true;
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
@@ -309,13 +309,13 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.service = ProgmemStringRef(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
resp.is_event = true;
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.key = ProgmemStringRef(StringRef(it.first));
|
||||
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
|
||||
kv.key = StringRef(it.first);
|
||||
kv.value = StringRef(it.second); // data map lives until send completes
|
||||
}
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -116,18 +116,42 @@ template<typename... Ts> class HomeAssistantServiceCallAction final : public Act
|
||||
const Field *fields = this->fields_;
|
||||
const size_t total = 1 + this->data_count_ + this->data_template_count_ + this->variables_count_;
|
||||
|
||||
// Lambda results must live until the send; the constant strings are encoded from flash where they are
|
||||
// Lambda results, and on ESP8266 the RAM copies of the flash strings, must live until the send
|
||||
size_t lambda_count = 0;
|
||||
for (size_t i = 0; i < total; i++)
|
||||
#ifdef USE_ESP8266
|
||||
size_t flash_len = 0;
|
||||
#endif
|
||||
for (size_t i = 0; i < total; i++) {
|
||||
lambda_count += fields[i].fn != nullptr;
|
||||
#ifdef USE_ESP8266
|
||||
if (fields[i].fn == nullptr)
|
||||
flash_len += ESPHOME_strlen_P(fields[i].value);
|
||||
if (fields[i].key != nullptr)
|
||||
flash_len += ESPHOME_strlen_P(fields[i].key);
|
||||
#endif
|
||||
}
|
||||
FixedVector<std::string> results;
|
||||
results.init(lambda_count);
|
||||
auto string_ref = [](const char *str) { return ProgmemStringRef(str, ESPHOME_strlen_P(str)); };
|
||||
#ifdef USE_ESP8266
|
||||
SmallBufferWithHeapFallback<128, char> flash_copy(flash_len);
|
||||
char *cursor = flash_copy.get();
|
||||
#endif
|
||||
auto string_ref = [&](const char *str) {
|
||||
#ifdef USE_ESP8266
|
||||
size_t len = ESPHOME_strlen_P(str);
|
||||
memcpy_P(cursor, str, len);
|
||||
StringRef ref(cursor, len);
|
||||
cursor += len;
|
||||
return ref;
|
||||
#else
|
||||
return StringRef(str);
|
||||
#endif
|
||||
};
|
||||
auto value_ref = [&](const Field &field) {
|
||||
if (field.fn == nullptr)
|
||||
return string_ref(field.value);
|
||||
results.push_back(field.fn(x...));
|
||||
return ProgmemStringRef(StringRef(results.back()));
|
||||
return StringRef(results.back());
|
||||
};
|
||||
auto fill = [&](FixedVector<HomeassistantServiceMap> &dest, uint8_t count) {
|
||||
dest.init(count);
|
||||
|
||||
@@ -99,7 +99,8 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
|
||||
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
|
||||
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
UserActionScratch scratch;
|
||||
auto resp = service->encode_list_service_response(scratch);
|
||||
if (!this->client_->send_message(resp))
|
||||
return false;
|
||||
// at_ is this service's index
|
||||
|
||||
@@ -418,18 +418,6 @@ class ProtoEncode {
|
||||
std::memcpy(pos + 2, ref.c_str(), ref.size());
|
||||
return pos + 2 + ref.size();
|
||||
}
|
||||
/// encode_short_string_force for a string that may be in flash (PROGMEM).
|
||||
[[nodiscard]] static inline uint8_t *encode_short_progmem_string_force(
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, const ProgmemStringRef &ref) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(ref.size() < 128 && "encode_short_progmem_string_force: string exceeds max_data_length < 128");
|
||||
#endif
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
|
||||
pos[0] = tag;
|
||||
pos[1] = static_cast<uint8_t>(ref.size());
|
||||
progmem_memcpy(pos + 2, ref.progmem_ptr(), ref.size());
|
||||
return pos + 2 + ref.size();
|
||||
}
|
||||
/// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
|
||||
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
|
||||
@@ -493,17 +481,6 @@ class ProtoEncode {
|
||||
progmem_memcpy(pos, data, len);
|
||||
return pos + len;
|
||||
}
|
||||
/// encode_progmem_bytes for a string field that may be in flash.
|
||||
[[nodiscard]] static inline uint8_t *encode_progmem_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const ProgmemStringRef &ref) {
|
||||
return encode_progmem_bytes(pos PROTO_ENCODE_DEBUG_ARG, field_id,
|
||||
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_progmem_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const ProgmemStringRef &ref) {
|
||||
return encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, field_id,
|
||||
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
@@ -703,20 +680,9 @@ class DumpBuffer {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Append len bytes of a PROGMEM string
|
||||
DumpBuffer &append_p(const char *str, size_t len) {
|
||||
#ifdef USE_ESP8266
|
||||
append_p_esp8266(str, len);
|
||||
#else
|
||||
append_impl_(str, len);
|
||||
#endif
|
||||
return *this;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
/// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site
|
||||
void append_p_esp8266(const char *str);
|
||||
void append_p_esp8266(const char *str, size_t len);
|
||||
#endif
|
||||
|
||||
const char *c_str() const { return buf_; }
|
||||
|
||||
@@ -1,36 +1,47 @@
|
||||
#include "user_services.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
ProgmemStringRef UserServiceStatic::str_(size_t idx) const {
|
||||
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
|
||||
const char *s = progmem_read_ptr(&this->strings_[idx]);
|
||||
if (s == nullptr)
|
||||
return {};
|
||||
return ProgmemStringRef(s, ESPHOME_strlen_P(s));
|
||||
#ifdef USE_ESP8266
|
||||
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
|
||||
if (scratch.empty())
|
||||
return {};
|
||||
size_t len = strnlen_P(s, scratch.size() - 1);
|
||||
progmem_memcpy(scratch.data(), s, len);
|
||||
scratch[len] = '\0';
|
||||
StringRef ref(scratch.data(), len);
|
||||
scratch = scratch.subspan(len + 1);
|
||||
return ref;
|
||||
#else
|
||||
return StringRef(s);
|
||||
#endif
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
|
||||
std::span<const enums::ServiceArgType> arg_types) const {
|
||||
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.name = this->str_(0);
|
||||
msg.name = this->str_(0, scratch);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = this->supports_response_;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
msg.description = this->str_(1);
|
||||
msg.description = this->str_(1, scratch);
|
||||
#endif
|
||||
msg.args.init(arg_types.size());
|
||||
for (size_t i = 0; i < arg_types.size(); i++) {
|
||||
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.name = this->str_(base);
|
||||
arg.name = this->str_(base, scratch);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
arg.description = this->str_(base + 1);
|
||||
arg.example = this->str_(base + 2);
|
||||
arg.description = this->str_(base + 1, scratch);
|
||||
arg.example = this->str_(base + 2, scratch);
|
||||
#endif
|
||||
}
|
||||
return msg;
|
||||
|
||||
@@ -20,8 +20,9 @@ class APIServer;
|
||||
|
||||
class UserServiceDescriptor {
|
||||
public:
|
||||
/// Build the list-entities message; its strings may point into flash (PROGMEM).
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
|
||||
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
|
||||
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
|
||||
|
||||
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
@@ -36,6 +37,13 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
||||
|
||||
template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||
|
||||
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
|
||||
#ifdef USE_ESP8266
|
||||
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
|
||||
#else
|
||||
using UserActionScratch = std::array<char, 0>;
|
||||
#endif
|
||||
|
||||
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
|
||||
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
|
||||
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
|
||||
@@ -55,9 +63,12 @@ class UserServiceStatic : public UserServiceDescriptor {
|
||||
: strings_(strings), key_(key), supports_response_(supports_response) {}
|
||||
|
||||
protected:
|
||||
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types) const;
|
||||
/// Table entry `idx` as it sits in flash; nullptr gives an empty string.
|
||||
ProgmemStringRef str_(size_t idx) const;
|
||||
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
|
||||
std::span<char> scratch) const;
|
||||
/// Reference table entry `idx`; nullptr gives an empty StringRef.
|
||||
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
|
||||
/// is advanced past the copy.
|
||||
StringRef str_(size_t idx, std::span<char> &scratch) const;
|
||||
|
||||
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
|
||||
uint32_t key_;
|
||||
@@ -68,9 +79,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceStatic {
|
||||
public:
|
||||
using UserServiceStatic::UserServiceStatic;
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
return this->encode_list_service_response_(arg_types);
|
||||
return this->encode_list_service_response_(arg_types, scratch);
|
||||
}
|
||||
|
||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||
@@ -114,9 +125,9 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
this->key_ = fnv1_hash(this->name_.c_str());
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.name = ProgmemStringRef(StringRef(this->name_));
|
||||
msg.name = StringRef(this->name_);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
@@ -124,7 +135,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.name = ProgmemStringRef(StringRef(this->arg_names_[i]));
|
||||
arg.name = StringRef(this->arg_names_[i]);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ AS3935_SCHEMA = cv.Schema(
|
||||
async def setup_as3935(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await cg.add_pin(var.set_irq_pin, config[CONF_IRQ_PIN])
|
||||
irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
|
||||
cg.add(var.set_irq_pin(irq_pin))
|
||||
cg.add(var.set_indoor(config[CONF_INDOOR]))
|
||||
cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL]))
|
||||
cg.add(var.set_watchdog_threshold(config[CONF_WATCHDOG_THRESHOLD]))
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
CONF_DIR_PIN,
|
||||
CONF_DIRECTION,
|
||||
CONF_HYSTERESIS,
|
||||
CONF_ID,
|
||||
CONF_POWER_MODE,
|
||||
CONF_RANGE,
|
||||
CONF_WATCHDOG,
|
||||
@@ -208,7 +209,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_direction(config[CONF_DIRECTION]))
|
||||
cg.add(var.set_watchdog(config[CONF_WATCHDOG]))
|
||||
@@ -218,7 +221,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_fast_filter(config[CONF_FAST_FILTER]))
|
||||
cg.add(var.set_start_position(config[CONF_START_POSITION]))
|
||||
|
||||
await cg.add_pin(var.set_dir_pin, config.get(CONF_DIR_PIN))
|
||||
if dir_pin_config := config.get(CONF_DIR_PIN):
|
||||
pin = await cg.gpio_pin_expression(dir_pin_config)
|
||||
cg.add(var.set_dir_pin(pin))
|
||||
|
||||
if (end_position_config := config.get(CONF_END_POSITION, None)) is not None:
|
||||
cg.add(var.set_end_position(end_position_config))
|
||||
|
||||
@@ -23,7 +23,7 @@ static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
|
||||
|
||||
void AS5600Component::setup() {
|
||||
if (!this->read_byte(REGISTER_STATUS).has_value()) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ void AS5600Component::setup() {
|
||||
|
||||
// write config to config register
|
||||
if (!this->write_byte_16(REGISTER_CONF, config)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,8 +87,10 @@ void AS5600Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AS5600:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed())
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Watchdog: %d\n"
|
||||
|
||||
@@ -30,7 +30,10 @@ CONF_END_POSITION = "end_position"
|
||||
CONF_OUT_OF_RANGE_MODE = "out_of_range_mode"
|
||||
|
||||
OutOfRangeMode = as5600_ns.enum("OutRangeMode")
|
||||
OUT_OF_RANGE_MODES = OutOfRangeMode.options("MIN_MAX", "NAN", prefix="OUT_RANGE_MODE_")
|
||||
OUT_OF_RANGE_MODES = {
|
||||
"MIN_MAX": OutOfRangeMode.OUT_RANGE_MODE_MIN_MAX,
|
||||
"NAN": OutOfRangeMode.OUT_RANGE_MODE_NAN,
|
||||
}
|
||||
|
||||
|
||||
CONF_AS5600_ID = "as5600_id"
|
||||
|
||||
@@ -9,12 +9,19 @@ ESPHOME_LOG_TAG(TAG, "as7341");
|
||||
void AS7341Component::setup() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (!this->check_chip_id(this, AS7341_ID, AS7341_CHIP_ID << 2, 0xFC))
|
||||
// Verify device ID
|
||||
uint8_t id;
|
||||
this->read_byte(AS7341_ID, &id);
|
||||
ESP_LOGCONFIG(TAG, " Read ID: 0x%X", id);
|
||||
if ((id & 0xFC) != (AS7341_CHIP_ID << 2)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Power on (enter IDLE state)
|
||||
if (!this->enable_power(true)) {
|
||||
this->mark_failed(LOG_STR("Power on failed"));
|
||||
ESP_LOGE(TAG, " Power on failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,6 +35,9 @@ void AS7341Component::setup() {
|
||||
void AS7341Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AS7341:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Gain: %u\n"
|
||||
@@ -95,16 +105,16 @@ uint8_t AS7341Component::get_atime() {
|
||||
}
|
||||
|
||||
uint16_t AS7341Component::get_astep() {
|
||||
uint16_t data = 0;
|
||||
this->read_byte_16_le(AS7341_ASTEP, &data);
|
||||
return data;
|
||||
uint16_t data;
|
||||
this->read_byte_16(AS7341_ASTEP, &data);
|
||||
return this->swap_bytes(data);
|
||||
}
|
||||
|
||||
bool AS7341Component::setup_gain(AS7341Gain gain) { return this->write_byte(AS7341_CFG1, gain); }
|
||||
|
||||
bool AS7341Component::setup_atime(uint8_t atime) { return this->write_byte(AS7341_ATIME, atime); }
|
||||
|
||||
bool AS7341Component::setup_astep(uint16_t astep) { return this->write_byte_16_le(AS7341_ASTEP, astep); }
|
||||
bool AS7341Component::setup_astep(uint16_t astep) { return this->write_byte_16(AS7341_ASTEP, swap_bytes(astep)); }
|
||||
|
||||
bool AS7341Component::read_channels(uint16_t *data) {
|
||||
this->set_smux_low_channels(true);
|
||||
@@ -253,4 +263,6 @@ bool AS7341Component::clear_register_bit(uint8_t address, uint8_t bit_position)
|
||||
return this->write_byte(address, data);
|
||||
}
|
||||
|
||||
uint16_t AS7341Component::swap_bytes(uint16_t data) { return (data >> 8) | (data << 8); }
|
||||
|
||||
} // namespace esphome::as7341
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user