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