mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 05:46:12 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f659814b2c | ||
|
|
82d09be042 | ||
|
|
fbbeb5cc4f | ||
|
|
469cfa296e | ||
|
|
eaad486cda | ||
|
|
15141a1e4f | ||
|
|
ec2245e595 | ||
|
|
494acce857 | ||
|
|
b4b3aae8b9 | ||
|
|
769c20a131 | ||
|
|
f29530ac47 | ||
|
|
5e2375aa01 | ||
|
|
3f6eafc880 | ||
|
|
2369ddea9e | ||
|
|
e0af4cace1 | ||
|
|
c464a48afb | ||
|
|
2c693fad5b | ||
|
|
3d8bfb7f94 |
@@ -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), `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).
|
||||
capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `homeassistant_light/` (direct bulb driver), `usb_audio/` (URL player).
|
||||
|
||||
## Templates in `$SKILL/templates`
|
||||
|
||||
@@ -130,9 +130,7 @@ capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `l
|
||||
`bk72xx-rtc-prefs-dut.yaml`, `bk72xx-crash-probe-dut.yaml`, `dut-ap-only-wipe-once-bk72xx.yaml`,
|
||||
`homeassistant-light-dut.yaml`, `deep-sleep-outgoing-dut.yaml`, `outgoing-wifi-drop-dut.yaml`,
|
||||
`globals-storage-dut.yaml`, `usb-audio-waveshare-s3-eth-dut.yaml`, `usb-audio-sendspin-s3-eth-dut.yaml`,
|
||||
`cast-pro-1-sendspin-control.yaml`, `aux-2-preprod-stock-dut.yaml`, `aux-2-preprod-usb-audio-dut.yaml`,
|
||||
`dfrobot-sen0395-dut.yaml`, `aux-2-preprod-speaker-player-dut.yaml`, `web-server-sse-dut.yaml`,
|
||||
`mqtt-transport-dut.yaml`.
|
||||
`cast-pro-1-sendspin-control.yaml`, `aux-2-preprod-stock-dut.yaml`, `aux-2-preprod-usb-audio-dut.yaml`.
|
||||
|
||||
## When something looks wrong
|
||||
|
||||
@@ -146,10 +144,6 @@ 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.
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ One file per component, named after it:
|
||||
- [`cse7766.md`](cse7766.md): Sonoff S31 parser check over the API
|
||||
- [`espnow.md`](espnow.md): ESP8266 broadcast receive
|
||||
- [`hdmi_cec.md`](hdmi_cec.md): two-board wire
|
||||
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback
|
||||
- [`ld2450.md`](ld2450.md): polygon zones
|
||||
- [`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, WebSocket transports with a local and a public broker
|
||||
- [`mqtt.md`](mqtt.md): broker bench, QoS, reconnect, crash log over MQTT
|
||||
- [`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
|
||||
@@ -25,8 +25,5 @@ One file per component, named after it:
|
||||
- [`zephyr_coredump.md`](zephyr_coredump.md): crash and decode on nRF52 boards
|
||||
- [`zwave_proxy.md`](zwave_proxy.md): frame checksums on a live proxy
|
||||
- [`globals.md`](globals.md): restoring globals with storage rtc and flash across every reset kind
|
||||
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant, held reports versus later calls
|
||||
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant
|
||||
- [`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
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# dfrobot_sen0395
|
||||
|
||||
## DFRobot SEN0395 mmWave on an ESP32 DevKit V1 (#20906, #20979)
|
||||
|
||||
Wiring: V to VIN (5 V, the module wants 3.6 to 5 V), G to GND, sensor TX to GPIO16 (RX2), sensor RX to GPIO17
|
||||
(TX2), IO2 to GPIO18 (presence output, a gpio binary sensor), I1 unconnected. `templates/dfrobot-sen0395-dut.yaml`
|
||||
has the hub, both detection binary sensors, the four switches, settings buttons and junk-line buttons.
|
||||
`scripts/dfrobot_sen0395/ctl.py HOST KEY press:NAME sw:NAME=on|off wait:S` drives it and prints binary sensor and
|
||||
switch updates; `cases.sh DEV_IP KEY CAPTURE LABEL` runs the whole flow against a running serial capture. Flash by
|
||||
USB, `serial_capture.py PORT 540 CAPTURE --reset`, then the cases; compare the `Sending command` and `Message:` lines
|
||||
and the counts of `Done`, `Error`, prompts and frames between builds. The module echoes every command, so the echoed
|
||||
lines are the bytes the component sent and can be compared between builds as a set.
|
||||
|
||||
Module facts: it answers `Done` or `Error` and then the `leapMMW:/>` prompt without a line end; a junk line gets
|
||||
`<line> is not recognized as a CLI command` (36 chars longer than the line, so 220 and 240 byte junk gives 256 and
|
||||
276 char replies); input lines of 256 bytes or more make the module assert (`..\User_code\main.c line:232`) and
|
||||
restart, after which the sensor is stopped; a factory reset restarts the module about 7 s later; `sensorStop` on a
|
||||
stopped sensor answers `sensor stopped already` then `Error`; a settings press takes 5 to 6 s (stop, change, save,
|
||||
start). Free heap on the ESP32 is Wi-Fi noise (192 to 198 kB) and says nothing about the component.
|
||||
Results: https://github.com/esphome/esphome/pull/20979#issuecomment-6279129146 and
|
||||
https://github.com/esphome/esphome/pull/20906#issuecomment-6279129604
|
||||
@@ -33,15 +33,3 @@ connected but not yet subscribed is dropped by the api server (`client has not s
|
||||
fallback to an off report the mirror keeps the unsent brightness; a mirror that never saw its light on since boot has
|
||||
no levels to restore. Results: https://github.com/esphome/esphome/pull/11354#issuecomment-6253630336 and
|
||||
https://github.com/esphome/esphome/pull/11354#issuecomment-6254161409
|
||||
|
||||
## Held report versus a later call (#21144)
|
||||
|
||||
A report that arrives during a transition is held and applied when the transition ends, unless a call came after
|
||||
it. `scripts/homeassistant_light/heldcase.py DEV_IP KEY BULB_IP same|many [BULB_KEY]` (a key of `-` is plaintext) keeps one API connection to the mirror
|
||||
device and one to the bulb so the steps land inside a 3 s fade: press "A 50 fade 3s", 0.7 s later set the bulb to
|
||||
90% directly (the report is held), 1.4 s in press the same fade again (or "A many calls", 16 calls in one loop
|
||||
pass). Pass: the mirror and the bulb end at the second call's target. Only `same` reproduces the bug: a build
|
||||
without the fix logs `Applying the report that arrived during the transition` and the bulb stays at 90%. `many`
|
||||
ends at 100% on both builds because the last call's target differs from the held report, so it is a regression
|
||||
check only, not a reproduction. Separate button presses are too slow (about 1.5 s each) to land inside the fade.
|
||||
The mirrors are not API entities, so read their state from the "Log state" button's line. Results: https://github.com/esphome/esphome/pull/21144#issuecomment-6278257695
|
||||
|
||||
@@ -5,11 +5,3 @@
|
||||
To make a target "leave" without anyone moving, set the radar's Zone-1 numbers over the target and
|
||||
switch the zone type to `Filter` (the count drops to 0 within 0.5 s); set it back to `Disabled` after.
|
||||
Writing those numbers blocks the ld2450 loop about 50 ms.
|
||||
|
||||
## Zone numbers round trip (#21146)
|
||||
|
||||
`scripts/ld2450/zonedrive.py HOST KEY(- for plaintext) set:NAME=VALUE sel:NAME=OPTION wait:S list` sets numbers and
|
||||
selects over the API and prints every LD2450 state update. After each set the hub sends the set zone command, queries
|
||||
the radar and republishes all twelve coordinates from the answer, so set each coordinate of each zone to a distinct
|
||||
value and check the readbacks; no target is needed. Readbacks can arrive late over Wi-Fi, so match them by content.
|
||||
Results: https://github.com/esphome/esphome/pull/21146#issuecomment-6278026922
|
||||
|
||||
@@ -10,22 +10,3 @@ 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
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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" [...] (KEY of - means a plaintext API)
|
||||
usage: api_button.py HOST KEY list | press "Button name" [...]
|
||||
"""
|
||||
|
||||
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=None if KEY == "-" else KEY)
|
||||
c = APIClient(HOST, 6053, None, noise_psk=KEY)
|
||||
await c.connect(login=True)
|
||||
ents, _ = await c.list_entities_services()
|
||||
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/sh
|
||||
# cases.sh DEV_IP KEY CAPTURE LABEL : the SEN0395 flow (settings, switches, junk lines, factory reset, reset) against
|
||||
# templates/dfrobot-sen0395-dut.yaml, with a serial capture of the device running into CAPTURE
|
||||
D=$1; K=$2; LOG=$3; LABEL=$4; PY=${PYTHON:-python3}; CTL="$(dirname "$0")/ctl.py"
|
||||
show() { tail -n +$(($1+1)) "$LOG" | sed 's/\x1b\[[0-9;]*m//g' | grep -a 'dfrobot\|Detected\|\[W\]\|\[E\]\|sen\]' | grep -a -v 'JYBSS\|took a long time' | cut -c1-200; }
|
||||
step() { n0=$(wc -l < "$LOG"); echo "=== $LABEL: $1 ($(date +%T))"; shift; $PY "$CTL" "$D" "$K" "$@" || { rc=$?; echo "driver failed ($rc), aborting"; exit $rc; }; echo "--- device log:"; show "$n0"; }
|
||||
frames() { echo "frames: $(tail -n +$(($1+1)) "$LOG" | grep -a -c 'JYBSS,1') present, $(tail -n +$(($1+1)) "$LOG" | grep -a -c 'JYBSS,0') absent"; }
|
||||
n0=$(wc -l < "$LOG"); echo "=== $LABEL: 20 s of frames ($(date +%T))"; sleep 20; frames "$n0"
|
||||
step "range 0 to 3 m" "press:Range 0 to 3m" wait:12
|
||||
step "range 0 to 9 m" "press:Range 0 to 9m" wait:12
|
||||
step "latency 1 s / 2 s" "press:Latency 1s 2s" wait:12
|
||||
step "latency 0 / 0" "press:Latency 0 0" wait:12
|
||||
step "sensitivity 7" "press:Sensitivity 7" wait:12
|
||||
step "LED off then on" "sw:LED=off" wait:10 "sw:LED=on" wait:10
|
||||
step "sensor active off then on" "sw:Sensor active=off" wait:10 "sw:Sensor active=on" wait:10
|
||||
step "presence via UART off then on" "sw:Presence via UART=off" wait:10 "sw:Presence via UART=on" wait:10
|
||||
step "start after boot off then on" "sw:Start after boot=off" wait:10 "sw:Start after boot=on" wait:10
|
||||
for n in 100 160 200 220 240 256; do step "junk line of $n bytes" "press:Junk $n" wait:7; done
|
||||
step "junk 220 then a command" "press:Junk 220" wait:6 "press:Range 0 to 9m" wait:12
|
||||
step "factory reset" "press:Factory reset" wait:15
|
||||
step "reset" "press:Reset" wait:15
|
||||
n0=$(wc -l < "$LOG"); echo "=== $LABEL: 20 s of frames after ($(date +%T))"; sleep 20; frames "$n0"
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Drive the SEN0395 test device over the API and print its binary sensor and switch updates.
|
||||
usage: ctl.py HOST KEY(- for plaintext) OP [OP ...] OP: press:NAME | sw:NAME=on|off | wait:SECONDS
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
BinarySensorState,
|
||||
ButtonInfo,
|
||||
SwitchInfo,
|
||||
SwitchState,
|
||||
)
|
||||
|
||||
HOST, KEY = sys.argv[1], sys.argv[2]
|
||||
OPS = sys.argv[3:]
|
||||
T0 = time.time()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
|
||||
await c.connect(login=True)
|
||||
ents, _ = await c.list_entities_services()
|
||||
by_key = {e.key: e for e in ents}
|
||||
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
|
||||
switches = {e.name: e for e in ents if isinstance(e, SwitchInfo)}
|
||||
|
||||
def on_state(s) -> None:
|
||||
e = by_key.get(s.key)
|
||||
if isinstance(s, BinarySensorState):
|
||||
print(
|
||||
f"{time.time() - T0:7.3f} BIN {e.name} = {'ON' if s.state else 'off'}"
|
||||
)
|
||||
elif isinstance(s, SwitchState):
|
||||
print(
|
||||
f"{time.time() - T0:7.3f} SW {e.name} = {'on' if s.state else 'off'}"
|
||||
)
|
||||
|
||||
c.subscribe_states(on_state)
|
||||
await asyncio.sleep(1.0)
|
||||
for op in OPS:
|
||||
kind, _, rest = op.partition(":")
|
||||
if kind == "wait":
|
||||
await asyncio.sleep(float(rest))
|
||||
elif kind == "press":
|
||||
if rest not in buttons:
|
||||
sys.exit(f"no button {rest!r}; have {sorted(buttons)}")
|
||||
print(f"{time.time() - T0:7.3f} >>> press {rest}")
|
||||
c.button_command(buttons[rest].key)
|
||||
elif kind == "sw":
|
||||
name, _, val = rest.partition("=")
|
||||
if name not in switches:
|
||||
sys.exit(f"no switch {name!r}; have {sorted(switches)}")
|
||||
print(f"{time.time() - T0:7.3f} >>> switch {name} {val}")
|
||||
c.switch_command(switches[name].key, val == "on")
|
||||
else:
|
||||
sys.exit(f"unknown op {op!r}")
|
||||
await c.disconnect()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,71 +0,0 @@
|
||||
"""Held-report cases for esphome#21144 with persistent API connections, so the steps land inside the 3 s fade.
|
||||
usage: heldcase.py DEV_IP KEY BULB_IP same|many [BULB_KEY] (KEY or BULB_KEY of - means plaintext)
|
||||
t0 press "A 50 fade 3s" on the mirror device (3 s fade to 50%)
|
||||
t0+0.7 set the bulb directly to 90% (Home Assistant reports it to the mirror during the fade: held)
|
||||
t0+1.4 same: press "A 50 fade 3s" again (same target as the running fade)
|
||||
many: press "A many calls" (16 calls in one loop pass, the last one 100% with a 1 s fade)
|
||||
t0+6.5 read the bulb, press Log state so the mirror prints its state
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import APIClient, ButtonInfo, LightInfo, LightState
|
||||
|
||||
DEV, KEY, BULB, CASE = sys.argv[1:5]
|
||||
BULB_KEY = sys.argv[5] if len(sys.argv) > 5 else "-"
|
||||
if CASE not in ("same", "many"):
|
||||
sys.exit(f"case must be same or many, got {CASE!r}")
|
||||
T0 = time.time()
|
||||
|
||||
|
||||
def stamp() -> str:
|
||||
return f"{time.time() - T0:6.3f}"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
dev = APIClient(DEV, 6053, None, noise_psk=None if KEY == "-" else KEY)
|
||||
bulb = APIClient(BULB, 6053, None, noise_psk=None if BULB_KEY == "-" else BULB_KEY)
|
||||
await dev.connect(login=True)
|
||||
await bulb.connect(login=True)
|
||||
dents, _ = await dev.list_entities_services()
|
||||
bents, _ = await bulb.list_entities_services()
|
||||
buttons = {e.name: e for e in dents if isinstance(e, ButtonInfo)}
|
||||
blight = next(e for e in bents if isinstance(e, LightInfo))
|
||||
states = {}
|
||||
|
||||
def on_bulb(s) -> None:
|
||||
if isinstance(s, LightState) and s.key == blight.key:
|
||||
states["bulb"] = s
|
||||
print(f"{stamp()} bulb state: on={s.state} bri={s.brightness:.2f}")
|
||||
|
||||
bulb.subscribe_states(on_bulb)
|
||||
await asyncio.sleep(1.0)
|
||||
print(f"{stamp()} >>> bulb to 100% (precondition)")
|
||||
bulb.light_command(blight.key, state=True, brightness=1.0)
|
||||
await asyncio.sleep(4.0)
|
||||
t = time.time()
|
||||
print(f"{stamp()} >>> t0 press A 50 fade 3s")
|
||||
dev.button_command(buttons["A 50 fade 3s"].key)
|
||||
await asyncio.sleep(0.7 - (time.time() - t))
|
||||
print(f"{stamp()} >>> bulb direct to 90%")
|
||||
bulb.light_command(blight.key, state=True, brightness=0.9)
|
||||
await asyncio.sleep(1.4 - (time.time() - t))
|
||||
name = "A 50 fade 3s" if CASE == "same" else "A many calls"
|
||||
print(f"{stamp()} >>> press {name}")
|
||||
dev.button_command(buttons[name].key)
|
||||
await asyncio.sleep(6.5 - (time.time() - t))
|
||||
print(f"{stamp()} >>> press Log state (the mirror's state is in the device log)")
|
||||
dev.button_command(buttons["Log state"].key)
|
||||
await asyncio.sleep(1.0)
|
||||
b = states.get("bulb")
|
||||
print(
|
||||
f"{stamp()} RESULT bulb "
|
||||
+ (f"bri={b.brightness:.2f}" if b is not None else "no state")
|
||||
)
|
||||
await dev.disconnect()
|
||||
await bulb.disconnect()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,85 +0,0 @@
|
||||
"""Drive LD2450 numbers and selects over the API while printing every LD2450 state update.
|
||||
usage: zonedrive.py HOST KEY(- for plaintext) OP [OP ...]
|
||||
OP: set:NAME=VALUE | sel:NAME=OPTION | wait:SECONDS | list
|
||||
Every state update of an entity whose name contains "LD2450" is printed with a timestamp.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
NumberInfo,
|
||||
NumberState,
|
||||
SelectInfo,
|
||||
SelectState,
|
||||
SensorInfo,
|
||||
SensorState,
|
||||
)
|
||||
|
||||
HOST, KEY = sys.argv[1], sys.argv[2]
|
||||
OPS = sys.argv[3:]
|
||||
T0 = time.time()
|
||||
|
||||
|
||||
def stamp() -> str:
|
||||
return f"{time.time() - T0:7.3f}"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
|
||||
await c.connect(login=True)
|
||||
ents, _ = await c.list_entities_services()
|
||||
by_key = {e.key: e for e in ents}
|
||||
numbers = {e.name: e for e in ents if isinstance(e, NumberInfo)}
|
||||
selects = {e.name: e for e in ents if isinstance(e, SelectInfo)}
|
||||
sensors = {e.name: e for e in ents if isinstance(e, SensorInfo)}
|
||||
|
||||
def on_state(s) -> None:
|
||||
e = by_key.get(s.key)
|
||||
if e is None or "LD2450" not in e.name:
|
||||
return
|
||||
if isinstance(s, NumberState):
|
||||
print(
|
||||
f"{stamp()} NUM {e.name} = {s.state:.0f}{' (missing)' if s.missing_state else ''}"
|
||||
)
|
||||
elif isinstance(s, SelectState):
|
||||
print(f"{stamp()} SEL {e.name} = {s.state}")
|
||||
elif isinstance(s, SensorState) and "Count" in e.name:
|
||||
print(f"{stamp()} CNT {e.name} = {s.state:.0f}")
|
||||
|
||||
c.subscribe_states(on_state)
|
||||
await asyncio.sleep(1.5)
|
||||
for op in OPS:
|
||||
if op == "list":
|
||||
for n in sorted(numbers):
|
||||
print("number:", n)
|
||||
for n in sorted(selects):
|
||||
print("select:", n, selects[n].options)
|
||||
for n in sorted(sensors):
|
||||
if "Count" in n:
|
||||
print("sensor:", n)
|
||||
continue
|
||||
kind, _, rest = op.partition(":")
|
||||
if kind == "wait":
|
||||
await asyncio.sleep(float(rest))
|
||||
continue
|
||||
name, _, value = rest.partition("=")
|
||||
if kind == "set":
|
||||
if name not in numbers:
|
||||
sys.exit(f"no number named {name!r}; have {sorted(numbers)}")
|
||||
print(f"{stamp()} >>> set {name} = {value}")
|
||||
c.number_command(numbers[name].key, float(value))
|
||||
elif kind == "sel":
|
||||
if name not in selects:
|
||||
sys.exit(f"no select named {name!r}; have {sorted(selects)}")
|
||||
print(f"{stamp()} >>> select {name} = {value}")
|
||||
c.select_command(selects[name].key, value)
|
||||
else:
|
||||
sys.exit(f"unknown op {op!r}")
|
||||
await asyncio.sleep(0.3)
|
||||
await c.disconnect()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,50 +0,0 @@
|
||||
"""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())
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/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 ]
|
||||
@@ -1,187 +0,0 @@
|
||||
"""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")
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,66 +0,0 @@
|
||||
# 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));
|
||||
@@ -1,186 +0,0 @@
|
||||
# DFRobot SEN0395 on an ESP32 DevKit V1: V to VIN (5 V), G to GND, sensor TX to GPIO16 (RX2), sensor RX to GPIO17 (TX2),
|
||||
# IO2 to GPIO18, I1 unconnected. Settings buttons, the four switches and junk-line buttons whose echo and
|
||||
# "is not recognized" reply come back as lines of known length (220 and 240 bytes give replies over 252 chars).
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
logs:
|
||||
api: DEBUG
|
||||
api.connection: DEBUG
|
||||
api.service: DEBUG
|
||||
wifi: DEBUG
|
||||
mdns: DEBUG
|
||||
scheduler: DEBUG
|
||||
component: DEBUG
|
||||
esp32: DEBUG
|
||||
uart: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_key
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
uart:
|
||||
id: mmwave_uart
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
baud_rate: 115200
|
||||
|
||||
dfrobot_sen0395:
|
||||
id: mmwave
|
||||
uart_id: mmwave_uart
|
||||
|
||||
binary_sensor:
|
||||
- platform: dfrobot_sen0395
|
||||
dfrobot_sen0395_id: mmwave
|
||||
name: Detected via UART
|
||||
id: uart_detected
|
||||
- platform: gpio
|
||||
name: Detected via GPIO
|
||||
device_class: motion
|
||||
pin:
|
||||
number: GPIO18
|
||||
mode: INPUT_PULLDOWN
|
||||
|
||||
switch:
|
||||
- platform: dfrobot_sen0395
|
||||
dfrobot_sen0395_id: mmwave
|
||||
type: sensor_active
|
||||
name: Sensor active
|
||||
- platform: dfrobot_sen0395
|
||||
dfrobot_sen0395_id: mmwave
|
||||
type: presence_via_uart
|
||||
name: Presence via UART
|
||||
- platform: dfrobot_sen0395
|
||||
dfrobot_sen0395_id: mmwave
|
||||
type: turn_on_led
|
||||
name: LED
|
||||
- platform: dfrobot_sen0395
|
||||
dfrobot_sen0395_id: mmwave
|
||||
type: start_after_boot
|
||||
name: Start after boot
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Range 0 to 3m
|
||||
on_press:
|
||||
- dfrobot_sen0395.settings:
|
||||
id: mmwave
|
||||
detection_segments:
|
||||
- [0cm, 3m]
|
||||
- platform: template
|
||||
name: Range 0 to 9m
|
||||
on_press:
|
||||
- dfrobot_sen0395.settings:
|
||||
id: mmwave
|
||||
detection_segments:
|
||||
- [0cm, 9m]
|
||||
- platform: template
|
||||
name: Latency 1s 2s
|
||||
on_press:
|
||||
- dfrobot_sen0395.settings:
|
||||
id: mmwave
|
||||
output_latency:
|
||||
delay_after_detect: 1s
|
||||
delay_after_disappear: 2s
|
||||
- platform: template
|
||||
name: Latency 0 0
|
||||
on_press:
|
||||
- dfrobot_sen0395.settings:
|
||||
id: mmwave
|
||||
output_latency:
|
||||
delay_after_detect: 0s
|
||||
delay_after_disappear: 0s
|
||||
- platform: template
|
||||
name: Sensitivity 7
|
||||
on_press:
|
||||
- dfrobot_sen0395.settings:
|
||||
id: mmwave
|
||||
sensitivity: 7
|
||||
- platform: template
|
||||
name: Factory reset
|
||||
on_press:
|
||||
- dfrobot_sen0395.settings:
|
||||
id: mmwave
|
||||
factory_reset: true
|
||||
- platform: template
|
||||
name: Reset
|
||||
on_press:
|
||||
- dfrobot_sen0395.reset:
|
||||
id: mmwave
|
||||
- platform: template
|
||||
name: Junk 100
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: mmwave_uart
|
||||
data: !lambda |-
|
||||
std::vector<uint8_t> v(100, 'x');
|
||||
v.push_back('\r');
|
||||
v.push_back('\n');
|
||||
return v;
|
||||
- platform: template
|
||||
name: Junk 160
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: mmwave_uart
|
||||
data: !lambda |-
|
||||
std::vector<uint8_t> v(160, 'x');
|
||||
v.push_back('\r');
|
||||
v.push_back('\n');
|
||||
return v;
|
||||
- platform: template
|
||||
name: Junk 200
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: mmwave_uart
|
||||
data: !lambda |-
|
||||
std::vector<uint8_t> v(200, 'x');
|
||||
v.push_back('\r');
|
||||
v.push_back('\n');
|
||||
return v;
|
||||
- platform: template
|
||||
name: Junk 220
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: mmwave_uart
|
||||
data: !lambda |-
|
||||
std::vector<uint8_t> v(220, 'x');
|
||||
v.push_back('\r');
|
||||
v.push_back('\n');
|
||||
return v;
|
||||
- platform: template
|
||||
name: Junk 240
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: mmwave_uart
|
||||
data: !lambda |-
|
||||
std::vector<uint8_t> v(240, 'x');
|
||||
v.push_back('\r');
|
||||
v.push_back('\n');
|
||||
return v;
|
||||
- platform: template
|
||||
name: Junk 256
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: mmwave_uart
|
||||
data: !lambda |-
|
||||
std::vector<uint8_t> v(256, 'x');
|
||||
v.push_back('\r');
|
||||
v.push_back('\n');
|
||||
return v;
|
||||
|
||||
interval:
|
||||
- interval: 30s
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("sen", "alive uart_detected=%d heap=%u", id(uart_detected).state, (unsigned) esp_get_free_heap_size());'
|
||||
@@ -124,20 +124,6 @@ button:
|
||||
name: B off
|
||||
on_press:
|
||||
- light.turn_off: bulb_b
|
||||
- platform: template
|
||||
name: A 50 fade 3s
|
||||
on_press:
|
||||
- light.turn_on:
|
||||
id: bulb_a
|
||||
brightness: 50%
|
||||
transition_length: 3s
|
||||
- platform: template
|
||||
name: A many calls
|
||||
on_press:
|
||||
- lambda: |-
|
||||
for (int i = 0; i < 16; i++) {
|
||||
id(bulb_a).make_call().set_brightness(i == 15 ? 1.0f : 0.6f).set_transition_length(1000).perform();
|
||||
}
|
||||
- platform: template
|
||||
name: Log state
|
||||
on_press:
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# 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()"]
|
||||
@@ -1,136 +0,0 @@
|
||||
# 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,6 +28,7 @@
|
||||
// python
|
||||
"ms-python.python",
|
||||
"ms-python.pylint",
|
||||
"ms-python.flake8",
|
||||
"charliermarsh.ruff",
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
// yaml
|
||||
@@ -47,6 +48,9 @@
|
||||
"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
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
[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
|
||||
@@ -313,7 +313,6 @@ jobs:
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
- "3.15"
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macOS-latest
|
||||
@@ -630,7 +629,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@821a40cd6c80e3cfe387caf55668287addd808da # v5.5.0
|
||||
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -14,4 +14,4 @@ jobs:
|
||||
permissions:
|
||||
issues: write # issues.lock on closed issues
|
||||
pull-requests: write # issues.lock on closed pull requests
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@31d160e5f0ffa0ee65d5b27cb55c153d415f2abe # 2026.10.2
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # 2026.9.0
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
|
||||
# GitHub App token so the labels, comments and closures come from
|
||||
# esphome[bot] instead of github-actions[bot].
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@31d160e5f0ffa0ee65d5b27cb55c153d415f2abe # main
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@4fbce13026c393d6eb6b247e394dcd9ef1af76c2 # main
|
||||
secrets:
|
||||
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
|
||||
@@ -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 (yamllint / ci-custom) is a
|
||||
# from a check-only hook (flake8 / yamllint / ci-custom) is a
|
||||
# real issue and fails the workflow loudly. Same PREK_SKIP list as
|
||||
# above for the same reasons.
|
||||
env:
|
||||
|
||||
@@ -10,13 +10,21 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.17.1
|
||||
rev: v0.17.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
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:
|
||||
|
||||
@@ -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` for linting and formatting. Configuration is in `pyproject.toml`.
|
||||
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
|
||||
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
|
||||
|
||||
* **Naming Conventions:**
|
||||
@@ -80,15 +80,13 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
```python
|
||||
# Bad - looks up CONF_BLAH twice
|
||||
if CONF_BLAH in config:
|
||||
cg.add(var.set_blah_ms(config[CONF_BLAH].total_milliseconds))
|
||||
cg.add(var.set_blah(config[CONF_BLAH]))
|
||||
|
||||
# Good - single lookup, value bound inline
|
||||
if (blah := config.get(CONF_BLAH)) is not None:
|
||||
cg.add(var.set_blah_ms(blah.total_milliseconds))
|
||||
cg.add(var.set_blah(blah))
|
||||
```
|
||||
The same applies to `while` loops and comprehensions where it avoids recomputing a value. Don't contort code to use it — reach for `:=` only when it genuinely cuts repetition or an extra assignment line.
|
||||
When the setter takes the value unchanged, use `cg.add_optional(var.set_blah, config.get(CONF_BLAH))`
|
||||
instead; it is not awaited, and it skips only `None`, so `0` and `False` are still set.
|
||||
|
||||
* **C++ Field Visibility:**
|
||||
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
|
||||
@@ -210,14 +208,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
* **Fixed sets of names** (status words, model names): a `PROGMEM_STRING_TABLE` (`esphome/core/progmem.h`),
|
||||
logged with `get_log_str` and `LOG_STR_ARG`, copied out with `ESPHOME_strncpy_P` before `publish_state`.
|
||||
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
|
||||
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `xor_bytes`, `sum16_bytes`,
|
||||
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`); pick the helper's
|
||||
arguments instead of writing a loop. Epson style 7-byte RTC calendars (one-hot weekday) use
|
||||
`time::decode_bcd_calendar`.
|
||||
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
|
||||
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
|
||||
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
|
||||
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `reverse_bits`, `encode_uint16`);
|
||||
pick the helper's polynomial and bit-order arguments instead of writing a CRC loop.
|
||||
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
|
||||
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
|
||||
end of `setup()` when the entities need a state at boot.
|
||||
@@ -437,22 +429,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
and `await cg.add_variable(var.set_bus, config[CONF_BUS_ID])`; both skip a `None` value, so pass
|
||||
`config.get(KEY)` for an optional key, and return what they created. Use `cg.register_parented` for `set_parent`.
|
||||
|
||||
- **Hub forwarding entities need no C++ class:** a button, switch, number or select that only calls its hub
|
||||
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
|
||||
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
|
||||
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state.
|
||||
```python
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
|
||||
```
|
||||
|
||||
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
|
||||
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
|
||||
constructs, registers and attaches in one call; `display.add_writer(var, config)` sets the lambda;
|
||||
`MyEnum.options("on", prefix="MY_ENUM_")` builds an enum dict; `button.restart_button_schema(cls)` (and
|
||||
`factory_reset_`, `query_`) are button presets; `fv.get_declaring_config(id)` finds a hub's config in
|
||||
final validation.
|
||||
|
||||
* **Automations (Triggers, Actions, Conditions):**
|
||||
|
||||
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
|
||||
|
||||
+1
-4
@@ -133,7 +133,7 @@ esphome/components/climate/* @esphome/core
|
||||
esphome/components/climate_ir/* @glmnet
|
||||
esphome/components/cm1106/* @andrewjswan
|
||||
esphome/components/color_temperature/* @jesserockz
|
||||
esphome/components/combination/* @kahrendt
|
||||
esphome/components/combination/* @Cat-Ion @kahrendt
|
||||
esphome/components/const/* @esphome/core
|
||||
esphome/components/coolix/* @glmnet
|
||||
esphome/components/copy/* @OttoWinter
|
||||
@@ -601,8 +601,6 @@ esphome/components/ssd1331_base/* @kbx81
|
||||
esphome/components/ssd1331_spi/* @kbx81
|
||||
esphome/components/ssd1351_base/* @kbx81
|
||||
esphome/components/ssd1351_spi/* @kbx81
|
||||
esphome/components/st25r/* @JohnMcLear
|
||||
esphome/components/st25r_spi/* @JohnMcLear
|
||||
esphome/components/st7123/* @miniskipper
|
||||
esphome/components/st7305/* @clydebarrow
|
||||
esphome/components/st7567_base/* @latonita
|
||||
@@ -697,7 +695,6 @@ esphome/components/veml3235/* @kbx81
|
||||
esphome/components/veml7700/* @latonita
|
||||
esphome/components/version/* @esphome/core
|
||||
esphome/components/vevor_dtx/* @Bofr5555
|
||||
esphome/components/vl53l1x/* @will-tm
|
||||
esphome/components/voice_assistant/* @jesserockz @kahrendt
|
||||
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.25.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.24.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -53,7 +53,6 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
with_local_variable,
|
||||
)
|
||||
from esphome.cpp_helpers import ( # noqa: F401
|
||||
add_optional,
|
||||
add_pin,
|
||||
add_variable,
|
||||
build_registry_entry,
|
||||
|
||||
@@ -184,9 +184,11 @@ 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_);
|
||||
}
|
||||
@@ -225,18 +227,24 @@ void AcDimmer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"AcDimmer:\n"
|
||||
" Min Power: %.1f%%\n"
|
||||
" 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"));
|
||||
" Init with half cycle: %s",
|
||||
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
|
||||
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,11 +6,19 @@
|
||||
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
enum DimMethod : uint8_t { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
|
||||
enum DimMethod { 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
|
||||
@@ -19,12 +27,6 @@ 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
|
||||
@@ -48,16 +50,17 @@ 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) { this->store_.method = method; }
|
||||
void set_method(DimMethod method) { method_ = method; }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
|
||||
InternalGPIOPin *gate_pin_;
|
||||
InternalGPIOPin *zero_cross_pin_;
|
||||
AcDimmerDataStore store_;
|
||||
gpio::InterruptType zero_cross_interrupt_type_;
|
||||
AcDimmerDataStore store_;
|
||||
bool init_with_half_cycle_;
|
||||
DimMethod method_;
|
||||
};
|
||||
|
||||
} // namespace esphome::ac_dimmer
|
||||
|
||||
@@ -136,7 +136,13 @@ void AirtonClimate::transmit_state() {
|
||||
data->set_carrier_frequency(AIRTON_IR_FREQUENCY);
|
||||
data->mark(AIRTON_HEADER_MARK);
|
||||
data->space(AIRTON_HEADER_SPACE);
|
||||
data->item_bytes<AIRTON_BIT_TIMINGS>(remote_state, AIRTON_STATE_FRAME_SIZE);
|
||||
for (uint8_t payload_byte : remote_state) {
|
||||
for (uint8_t payload_bit_cursor = 0; payload_bit_cursor < 8; payload_bit_cursor++) {
|
||||
data->mark(AIRTON_BIT_MARK);
|
||||
bool bit = payload_byte & (1 << payload_bit_cursor);
|
||||
data->space(bit ? AIRTON_ONE_SPACE : AIRTON_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
data->mark(AIRTON_BIT_MARK);
|
||||
data->space(AIRTON_MESSAGE_SPACE);
|
||||
@@ -278,14 +284,20 @@ void AirtonClimate::parse_state_frame_(const uint8_t *frame) {
|
||||
}
|
||||
|
||||
bool AirtonClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t remote_state[AIRTON_STATE_FRAME_SIZE];
|
||||
uint8_t remote_state[AIRTON_STATE_FRAME_SIZE] = {};
|
||||
if (!data.expect_item(AIRTON_HEADER_MARK, AIRTON_HEADER_SPACE)) {
|
||||
ESP_LOGV(TAG, "Wrong header encoding detected!");
|
||||
return false;
|
||||
}
|
||||
if (!data.expect_bytes<AIRTON_BIT_TIMINGS>(remote_state, AIRTON_STATE_FRAME_SIZE)) {
|
||||
ESP_LOGV(TAG, "Wrong modulation encoding");
|
||||
return false;
|
||||
for (int i = 0; i < AIRTON_STATE_FRAME_SIZE; i++) {
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (data.expect_item(AIRTON_BIT_MARK, AIRTON_ONE_SPACE)) {
|
||||
remote_state[i] |= 1 << j;
|
||||
} else if (!data.expect_item(AIRTON_BIT_MARK, AIRTON_ZERO_SPACE)) {
|
||||
ESP_LOGV(TAG, "Wrong modulation encoding for: Byte %d, bit %d", i, j);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (remote_state[0] != AIRTON_HEADER_BYTE0 || remote_state[1] != AIRTON_HEADER_BYTE1) {
|
||||
|
||||
@@ -36,8 +36,8 @@ static constexpr uint32_t AIRTON_IR_FREQUENCY = 38000;
|
||||
static constexpr uint32_t AIRTON_HEADER_MARK = 6630;
|
||||
static constexpr uint32_t AIRTON_HEADER_SPACE = 3350;
|
||||
static constexpr uint32_t AIRTON_BIT_MARK = 400;
|
||||
static constexpr remote_base::BitTimings AIRTON_BIT_TIMINGS{
|
||||
.bit_mark = AIRTON_BIT_MARK, .one_space = 1260, .zero_space = 430};
|
||||
static constexpr uint32_t AIRTON_ONE_SPACE = 1260;
|
||||
static constexpr uint32_t AIRTON_ZERO_SPACE = 430;
|
||||
static constexpr uint32_t AIRTON_MESSAGE_SPACE = 100000;
|
||||
|
||||
static constexpr uint8_t AIRTON_STATE_FRAME_SIZE = 7;
|
||||
|
||||
@@ -8,18 +8,19 @@ from ..climate import CONF_AIRTON_ID, CONF_SLEEP_MODE, AirtonClimate, airton_ns
|
||||
|
||||
CODEOWNERS = ["@Procsiab"]
|
||||
|
||||
AirtonSwitch = airton_ns.class_("AirtonSwitch", switch.Switch)
|
||||
AirtonSetting = airton_ns.enum("AirtonSetting", is_class=True)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_AIRTON_ID): cv.use_id(AirtonClimate),
|
||||
cv.Optional(CONF_DISPLAY): switch.switch_schema(
|
||||
switch.ApplySwitch,
|
||||
AirtonSwitch,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
default_restore_mode="DISABLED",
|
||||
),
|
||||
cv.Optional(CONF_SLEEP_MODE): switch.switch_schema(
|
||||
switch.ApplySwitch,
|
||||
AirtonSwitch,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
default_restore_mode="DISABLED",
|
||||
),
|
||||
@@ -31,12 +32,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_AIRTON_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(
|
||||
CONF_DISPLAY,
|
||||
hub.set_display_switch,
|
||||
apply=f"set_setting({AirtonSetting.AIRTON_SETTING_DISPLAY}, state)",
|
||||
CONF_DISPLAY, hub.set_display_switch, AirtonSetting.AIRTON_SETTING_DISPLAY
|
||||
)
|
||||
await switches(
|
||||
CONF_SLEEP_MODE,
|
||||
hub.set_sleep_mode_switch,
|
||||
apply=f"set_setting({AirtonSetting.AIRTON_SETTING_SLEEP_MODE}, state)",
|
||||
AirtonSetting.AIRTON_SETTING_SLEEP_MODE,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../airton.h"
|
||||
|
||||
namespace esphome::airton {
|
||||
|
||||
// One of the unit's on/off settings; the parent sends the state and publishes it back
|
||||
class AirtonSwitch : public switch_::Switch, public Parented<AirtonClimate> {
|
||||
public:
|
||||
explicit AirtonSwitch(AirtonSetting setting) : setting_(setting) {}
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override { this->parent_->set_setting(this->setting_, state); }
|
||||
|
||||
AirtonSetting setting_;
|
||||
};
|
||||
|
||||
} // namespace esphome::airton
|
||||
@@ -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, CONF_ERROR_CODE
|
||||
from esphome.components.const import CONF_AQI
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ECO2,
|
||||
@@ -55,6 +55,7 @@ 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)
|
||||
|
||||
@@ -306,6 +306,11 @@ 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]
|
||||
@@ -440,11 +445,6 @@ 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,
|
||||
@@ -728,6 +728,15 @@ 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.",
|
||||
@@ -762,7 +771,6 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@@ -119,6 +119,11 @@ 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
|
||||
|
||||
|
||||
@@ -145,7 +150,6 @@ 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(
|
||||
|
||||
@@ -17,6 +17,16 @@ 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,
|
||||
@@ -35,13 +45,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
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",
|
||||
),
|
||||
_validate_extended_range,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -637,18 +637,6 @@ float ATM90E32Component::get_chip_temperature_() {
|
||||
return (float) ctemp;
|
||||
}
|
||||
|
||||
void ATM90E32Component::press_calibration_button(EntityBase *entity, void (ATM90E32Component::*action)(),
|
||||
const LogString *hint, const LogString *hint2) {
|
||||
ESP_LOGI(TAG, "%s", entity->get_name().c_str());
|
||||
if (hint != nullptr) {
|
||||
ESP_LOGI(TAG, "%s", LOG_STR_ARG(hint));
|
||||
}
|
||||
if (hint2 != nullptr) {
|
||||
ESP_LOGI(TAG, "%s", LOG_STR_ARG(hint2));
|
||||
}
|
||||
(this->*action)();
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_gain_calibrations() {
|
||||
const char *cs = this->get_calibration_id_();
|
||||
if (!this->enable_gain_calibration_) {
|
||||
|
||||
@@ -139,9 +139,6 @@ class ATM90E32Component final : public PollingComponent,
|
||||
int16_t calibrate_offset(uint8_t phase, bool voltage);
|
||||
int16_t calibrate_power_offset(uint8_t phase, bool reactive);
|
||||
void run_gain_calibrations();
|
||||
// Body of the calibration buttons: logs the button name and the hint lines, then runs action.
|
||||
void press_calibration_button(EntityBase *entity, void (ATM90E32Component::*action)(),
|
||||
const LogString *hint = nullptr, const LogString *hint2 = nullptr);
|
||||
#ifdef USE_NUMBER
|
||||
void set_reference_voltage(uint8_t phase, number::Number *ref_voltage) { ref_voltages_[phase] = ref_voltage; }
|
||||
void set_reference_current(uint8_t phase, number::Number *ref_current) { ref_currents_[phase] = ref_current; }
|
||||
|
||||
@@ -4,6 +4,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import atm90e32_ns
|
||||
from ..sensor import ATM90E32Component
|
||||
|
||||
CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration"
|
||||
@@ -13,68 +14,83 @@ CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration"
|
||||
CONF_RUN_POWER_OFFSET_CALIBRATION = "run_power_offset_calibration"
|
||||
CONF_CLEAR_POWER_OFFSET_CALIBRATION = "clear_power_offset_calibration"
|
||||
|
||||
|
||||
def _calibration_button(icon: str) -> cv.Schema:
|
||||
return button.button_schema(
|
||||
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=icon
|
||||
)
|
||||
|
||||
ATM90E32GainCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32GainCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32ClearGainCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32ClearGainCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32OffsetCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32OffsetCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32ClearOffsetCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32ClearOffsetCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32PowerOffsetCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32PowerOffsetCalibrationButton", button.Button
|
||||
)
|
||||
ATM90E32ClearPowerOffsetCalibrationButton = atm90e32_ns.class_(
|
||||
"ATM90E32ClearPowerOffsetCalibrationButton", button.Button
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
|
||||
cv.Optional(CONF_RUN_GAIN_CALIBRATION): _calibration_button("mdi:scale-balance"),
|
||||
cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): _calibration_button("mdi:delete"),
|
||||
cv.Optional(CONF_RUN_OFFSET_CALIBRATION): _calibration_button(ICON_SCALE),
|
||||
cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): _calibration_button("mdi:delete"),
|
||||
cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): _calibration_button(ICON_SCALE),
|
||||
cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): _calibration_button("mdi:delete"),
|
||||
cv.Optional(CONF_RUN_GAIN_CALIBRATION): button.button_schema(
|
||||
ATM90E32GainCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:scale-balance",
|
||||
),
|
||||
cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): button.button_schema(
|
||||
ATM90E32ClearGainCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:delete",
|
||||
),
|
||||
cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema(
|
||||
ATM90E32OffsetCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_SCALE,
|
||||
),
|
||||
cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema(
|
||||
ATM90E32ClearOffsetCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:delete",
|
||||
),
|
||||
cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): button.button_schema(
|
||||
ATM90E32PowerOffsetCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_SCALE,
|
||||
),
|
||||
cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): button.button_schema(
|
||||
ATM90E32ClearPowerOffsetCalibrationButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:delete",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _press(action: str, hint: str | None = None, hint2: str | None = None) -> str:
|
||||
"""press_calibration_button() logs at most two hint lines."""
|
||||
args = "".join(
|
||||
f", {cg.LogStringLiteral(h)}" for h in (hint, hint2) if h is not None
|
||||
)
|
||||
return f"press_calibration_button(entity, &{ATM90E32Component}::{action}{args})"
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
buttons = button.sub_buttons(config, parent=config[CONF_ID])
|
||||
await buttons(
|
||||
CONF_RUN_GAIN_CALIBRATION,
|
||||
None,
|
||||
apply=_press(
|
||||
"run_gain_calibrations",
|
||||
"[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values",
|
||||
),
|
||||
)
|
||||
await buttons(
|
||||
CONF_CLEAR_GAIN_CALIBRATION, None, apply=_press("clear_gain_calibrations")
|
||||
)
|
||||
await buttons(
|
||||
CONF_RUN_OFFSET_CALIBRATION,
|
||||
None,
|
||||
apply=_press(
|
||||
"run_offset_calibrations",
|
||||
"[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**",
|
||||
"[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save these values",
|
||||
),
|
||||
)
|
||||
await buttons(
|
||||
CONF_CLEAR_OFFSET_CALIBRATION, None, apply=_press("clear_offset_calibrations")
|
||||
)
|
||||
await buttons(
|
||||
CONF_RUN_POWER_OFFSET_CALIBRATION,
|
||||
None,
|
||||
apply=_press(
|
||||
"run_power_offset_calibrations",
|
||||
"[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**",
|
||||
"[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config file to save these values",
|
||||
),
|
||||
)
|
||||
await buttons(
|
||||
CONF_CLEAR_POWER_OFFSET_CALIBRATION,
|
||||
None,
|
||||
apply=_press("clear_power_offset_calibrations"),
|
||||
)
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION):
|
||||
b = await button.new_button(run_gain)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if clear_gain := config.get(CONF_CLEAR_GAIN_CALIBRATION):
|
||||
b = await button.new_button(clear_gain)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION):
|
||||
b = await button.new_button(run_offset)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION):
|
||||
b = await button.new_button(clear_offset)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if run_power := config.get(CONF_RUN_POWER_OFFSET_CALIBRATION):
|
||||
b = await button.new_button(run_power)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
if clear_power := config.get(CONF_CLEAR_POWER_OFFSET_CALIBRATION):
|
||||
b = await button.new_button(clear_power)
|
||||
await cg.register_parented(b, parent)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "atm90e32_button.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::atm90e32 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "atm90e32.button");
|
||||
|
||||
void ATM90E32GainCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Gain Calibration button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
ESP_LOGI(TAG,
|
||||
"[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values");
|
||||
this->parent_->run_gain_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32ClearGainCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Gain button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
this->parent_->clear_gain_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32OffsetCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Offset Calibration button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save "
|
||||
"these values");
|
||||
this->parent_->run_offset_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32ClearOffsetCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Offset button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
this->parent_->clear_offset_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32PowerOffsetCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Power Calibration button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config "
|
||||
"file to save these values");
|
||||
this->parent_->run_power_offset_calibrations();
|
||||
}
|
||||
|
||||
void ATM90E32ClearPowerOffsetCalibrationButton::press_action() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Power button [%s]", this->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", this->get_name().c_str());
|
||||
this->parent_->clear_power_offset_calibrations();
|
||||
}
|
||||
|
||||
} // namespace esphome::atm90e32
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/atm90e32/atm90e32.h"
|
||||
#include "esphome/components/button/button.h"
|
||||
|
||||
namespace esphome::atm90e32 {
|
||||
|
||||
class ATM90E32GainCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32GainCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32ClearGainCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32ClearGainCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32OffsetCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32OffsetCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32ClearOffsetCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32ClearOffsetCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32PowerOffsetCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32PowerOffsetCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
class ATM90E32ClearPowerOffsetCalibrationButton final : public button::Button, public Parented<ATM90E32Component> {
|
||||
public:
|
||||
ATM90E32ClearPowerOffsetCalibrationButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::atm90e32
|
||||
@@ -8,8 +8,8 @@ ESPHOME_LOG_TAG(TAG, "ballu.climate");
|
||||
const uint16_t BALLU_HEADER_MARK = 9000;
|
||||
const uint16_t BALLU_HEADER_SPACE = 4500;
|
||||
const uint16_t BALLU_BIT_MARK = 575;
|
||||
static constexpr remote_base::BitTimings BALLU_BIT_TIMINGS{
|
||||
.bit_mark = BALLU_BIT_MARK, .one_space = 1675, .zero_space = 550};
|
||||
const uint16_t BALLU_ONE_SPACE = 1675;
|
||||
const uint16_t BALLU_ZERO_SPACE = 550;
|
||||
|
||||
const uint32_t BALLU_CARRIER_FREQUENCY = 38000;
|
||||
|
||||
@@ -104,7 +104,13 @@ void BalluClimate::transmit_state() {
|
||||
data->mark(BALLU_HEADER_MARK);
|
||||
data->space(BALLU_HEADER_SPACE);
|
||||
// Data
|
||||
data->item_bytes<BALLU_BIT_TIMINGS>(remote_state, BALLU_STATE_LENGTH);
|
||||
for (uint8_t i : remote_state) {
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
data->mark(BALLU_BIT_MARK);
|
||||
bool bit = i & (1 << j);
|
||||
data->space(bit ? BALLU_ONE_SPACE : BALLU_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
// Footer
|
||||
data->mark(BALLU_BIT_MARK);
|
||||
|
||||
@@ -118,10 +124,21 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t remote_state[BALLU_STATE_LENGTH];
|
||||
if (!data.expect_bytes<BALLU_BIT_TIMINGS>(remote_state, BALLU_STATE_LENGTH)) {
|
||||
ESP_LOGV(TAG, "Data fail");
|
||||
return false;
|
||||
uint8_t remote_state[BALLU_STATE_LENGTH] = {0};
|
||||
// Read all bytes.
|
||||
for (int i = 0; i < BALLU_STATE_LENGTH; i++) {
|
||||
// Read bit
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (data.expect_item(BALLU_BIT_MARK, BALLU_ONE_SPACE)) {
|
||||
remote_state[i] |= 1 << j;
|
||||
|
||||
} else if (!data.expect_item(BALLU_BIT_MARK, BALLU_ZERO_SPACE)) {
|
||||
ESP_LOGV(TAG, "Byte %d bit %d fail", i, j);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "Byte %d %02X", i, remote_state[i]);
|
||||
}
|
||||
// Validate footer
|
||||
if (!data.expect_mark(BALLU_BIT_MARK)) {
|
||||
|
||||
@@ -43,4 +43,5 @@ async def register_bedjet_child(var: MockObj, config: ConfigType) -> None:
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await ble_client.new_ble_node_component(config)
|
||||
await cg.add_variable(var.set_time_id, config.get(CONF_TIME_ID))
|
||||
cg.add_optional(var.set_status_timeout, config.get(CONF_RECEIVE_TIMEOUT))
|
||||
if (receive_timeout := config.get(CONF_RECEIVE_TIMEOUT)) is not None:
|
||||
cg.add(var.set_status_timeout(receive_timeout))
|
||||
|
||||
@@ -111,7 +111,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
|
||||
cg.add(var.set_pin(config[CONF_PIN]))
|
||||
|
||||
cg.add_optional(var.set_max_refresh_rate, config.get(CONF_MAX_REFRESH_RATE))
|
||||
if CONF_MAX_REFRESH_RATE in config:
|
||||
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
|
||||
|
||||
chipset = CHIPSETS[config[CONF_CHIPSET]]
|
||||
cg.add(
|
||||
|
||||
@@ -9,9 +9,11 @@ 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 (!log_entity_header(tag, prefix, type, obj)) {
|
||||
if (obj == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@ float BL0940::calculate_energy_reference_() {
|
||||
float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; }
|
||||
|
||||
void BL0940::reset_calibration() {
|
||||
ESP_LOGI(TAG, "Resetting calibration defaults...");
|
||||
#ifdef USE_NUMBER
|
||||
if (this->current_calibration_number_ != nullptr && this->current_cal_ != 1) {
|
||||
this->current_calibration_number_->make_call().set_value(0).perform();
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_BL0940_ID
|
||||
from .. import CONF_BL0940_ID, bl0940_ns
|
||||
from ..sensor import BL0940
|
||||
|
||||
CONFIG_SCHEMA = button.button_schema(
|
||||
button.ApplyButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_RESTART,
|
||||
).extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)})
|
||||
CalibrationResetButton = bl0940_ns.class_(
|
||||
"CalibrationResetButton", button.Button, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
button.button_schema(
|
||||
CalibrationResetButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_RESTART,
|
||||
)
|
||||
.extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await button.sub_buttons(config, parent=config[CONF_BL0940_ID]).create(
|
||||
config, None, apply="reset_calibration()"
|
||||
)
|
||||
var = await button.new_button(config)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_BL0940_ID])
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#include "calibration_reset_button.h"
|
||||
#include "../bl0940.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::bl0940 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bl0940.button.calibration_reset");
|
||||
|
||||
void CalibrationResetButton::dump_config() { LOG_BUTTON("", "Calibration Reset Button", this); }
|
||||
|
||||
void CalibrationResetButton::press_action() {
|
||||
ESP_LOGI(TAG, "Resetting calibration defaults...");
|
||||
this->parent_->reset_calibration();
|
||||
}
|
||||
|
||||
} // namespace esphome::bl0940
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/button/button.h"
|
||||
|
||||
namespace esphome::bl0940 {
|
||||
|
||||
class BL0940; // Forward declaration of BL0940 class
|
||||
|
||||
class CalibrationResetButton final : public button::Button, public Component, public Parented<BL0940> {
|
||||
public:
|
||||
void dump_config() override;
|
||||
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::bl0940
|
||||
@@ -91,6 +91,23 @@ 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)
|
||||
@@ -136,7 +153,7 @@ def set_reference_values(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BL0940),
|
||||
@@ -191,18 +208,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.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),
|
||||
cv.only_when(
|
||||
(
|
||||
CONF_REFERENCE_VOLTAGE,
|
||||
CONF_RESISTOR_SHUNT,
|
||||
CONF_RESISTOR_ONE,
|
||||
CONF_RESISTOR_TWO,
|
||||
),
|
||||
CONF_LEGACY_MODE,
|
||||
False,
|
||||
),
|
||||
.add_extra(set_reference_values)
|
||||
)
|
||||
|
||||
# BL0940 datasheet: 4800 baud, 8 data bits, no parity (stop bits are 1.5 -- not
|
||||
|
||||
@@ -83,6 +83,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
ibeacon_uuid = ble_device_base.as_reversed_hex_array(ibeacon_uuid)
|
||||
cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
|
||||
|
||||
cg.add_optional(var.set_ibeacon_major, config.get(CONF_IBEACON_MAJOR))
|
||||
if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:
|
||||
cg.add(var.set_ibeacon_major(ibeacon_major))
|
||||
|
||||
cg.add_optional(var.set_ibeacon_minor, config.get(CONF_IBEACON_MINOR))
|
||||
if (ibeacon_minor := config.get(CONF_IBEACON_MINOR)) is not None:
|
||||
cg.add(var.set_ibeacon_minor(ibeacon_minor))
|
||||
|
||||
@@ -79,6 +79,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
ibeacon_uuid = ble_device_base.as_reversed_hex_array(ibeacon_uuid)
|
||||
cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
|
||||
|
||||
cg.add_optional(var.set_ibeacon_major, config.get(CONF_IBEACON_MAJOR))
|
||||
if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:
|
||||
cg.add(var.set_ibeacon_major(ibeacon_major))
|
||||
|
||||
cg.add_optional(var.set_ibeacon_minor, config.get(CONF_IBEACON_MINOR))
|
||||
if (ibeacon_minor := config.get(CONF_IBEACON_MINOR)) is not None:
|
||||
cg.add(var.set_ibeacon_minor(ibeacon_minor))
|
||||
|
||||
@@ -94,14 +94,24 @@ 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 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)],
|
||||
}
|
||||
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
|
||||
|
||||
return cv.All(
|
||||
(
|
||||
@@ -126,7 +136,6 @@ 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_,
|
||||
ESP_LOGV(TAG, "%s: updating subscription for %d virtual sensors (out=%d sensors)", this->device_id_.c_str(),
|
||||
num_virtual_sensors, num_sensor_settings);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "%s via BSEC:", this->device_id_);
|
||||
ESP_LOGCONFIG(TAG, "%s via BSEC:", this->device_id_.c_str());
|
||||
|
||||
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_);
|
||||
ESP_LOGV(TAG, "%s: Performing sensor run", this->device_id_.c_str());
|
||||
|
||||
// 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_);
|
||||
ESP_LOGV(TAG, "%s: Reading data", this->device_id_.c_str());
|
||||
int64_t curr_time_ns = this->get_time_ns_();
|
||||
|
||||
if (this->bme680_settings_.trigger_measurement) {
|
||||
@@ -365,7 +365,8 @@ 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_, num_inputs, num_outputs);
|
||||
ESP_LOGV(TAG, "%s: after bsec_do_steps: num_inputs=%d num_outputs=%d", this->device_id_.c_str(), 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)
|
||||
@@ -380,7 +381,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_);
|
||||
ESP_LOGV(TAG, "%s: Queuing sensor state publish actions", this->device_id_.c_str());
|
||||
for (uint8_t i = 0; i < num_outputs; i++) {
|
||||
float signal = outputs[i].signal;
|
||||
switch (outputs[i].sensor_id) {
|
||||
@@ -470,7 +471,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_,
|
||||
ESP_LOGW(TAG, "%s: Failed to fetch BSEC library state for snapshot (BSEC Error Code %d)", this->device_id_.c_str(),
|
||||
this->bsec_status_);
|
||||
return;
|
||||
}
|
||||
@@ -481,7 +482,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_);
|
||||
ESP_LOGV(TAG, "%s: BSEC state data NOT valid, aborting restore_state_()", this->device_id_.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -526,16 +527,17 @@ void BME680BSECComponent::load_state_() {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "%s: Loading BSEC library state", this->device_id_);
|
||||
ESP_LOGV(TAG, "%s: Loading BSEC library state", this->device_id_.c_str());
|
||||
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_, this->bsec_status_);
|
||||
ESP_LOGW(TAG, "%s: Failed to load BSEC library state (BSEC Error Code %d)", this->device_id_.c_str(),
|
||||
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_);
|
||||
ESP_LOGI(TAG, "%s: Loaded BSEC library state", this->device_id_.c_str());
|
||||
}
|
||||
|
||||
void BME680BSECComponent::save_state_(uint8_t accuracy) {
|
||||
@@ -550,7 +552,7 @@ void BME680BSECComponent::save_state_(uint8_t accuracy) {
|
||||
if (!this->bsec_state_data_valid_)
|
||||
return;
|
||||
|
||||
ESP_LOGV(TAG, "%s: Saving state", this->device_id_);
|
||||
ESP_LOGV(TAG, "%s: Saving state", this->device_id_.c_str());
|
||||
|
||||
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 char *devid) { this->device_id_ = devid; }
|
||||
void set_device_id(const std::string &devid) { this->device_id_.assign(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_;
|
||||
|
||||
const char *device_id_{nullptr};
|
||||
std::string device_id_;
|
||||
float temperature_offset_{0};
|
||||
IAQMode iaq_mode_{IAQ_MODE_STATIC};
|
||||
SupplyVoltage supply_voltage_;
|
||||
|
||||
@@ -7,9 +7,11 @@ 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 (!log_entity_header(tag, prefix, type, obj)) {
|
||||
if (obj == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||
LOG_ENTITY_ICON(tag, prefix, *obj);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
await text_sensors(CONF_VERSION, var.set_version)
|
||||
|
||||
cg.add_optional(var.set_baseline, config.get(CONF_BASELINE))
|
||||
if (baseline := config.get(CONF_BASELINE)) is not None:
|
||||
cg.add(var.set_baseline(baseline))
|
||||
|
||||
await cg.add_variable(var.set_temperature, config.get(CONF_TEMPERATURE))
|
||||
await cg.add_variable(var.set_humidity, config.get(CONF_HUMIDITY))
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
|
||||
namespace esphome::climate {
|
||||
|
||||
#define LOG_CLIMATE(prefix, type, obj) log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))
|
||||
#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()); \
|
||||
}
|
||||
|
||||
class Climate;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from esphome.const import (
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
|
||||
|
||||
combination_ns = cg.esphome_ns.namespace("combination")
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ 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"
|
||||
@@ -34,7 +33,6 @@ 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"
|
||||
@@ -50,7 +48,6 @@ 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"
|
||||
@@ -68,7 +65,6 @@ CONF_REMOTE_ID = "remote_id"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_ROLE = "role"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_RST_PIN = "rst_pin"
|
||||
CONF_SCAN_PARAMETERS = "scan_parameters"
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
CONF_SHA256 = "sha256"
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_SOURCE_ID,
|
||||
)
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONFIG_SCHEMA = button.button_schema(button.ApplyButton).extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button),
|
||||
}
|
||||
from .. import copy_ns
|
||||
|
||||
CopyButton = copy_ns.class_("CopyButton", button.Button, cg.Component)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
button.button_schema(CopyButton)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
@@ -23,6 +34,8 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await button.sub_buttons(config, parent=config[CONF_SOURCE_ID]).create(
|
||||
config, None, apply="press()"
|
||||
)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await button.register_button(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "copy_button.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::copy {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "copy.button");
|
||||
|
||||
void CopyButton::dump_config() { LOG_BUTTON("", "Copy Button", this); }
|
||||
|
||||
void CopyButton::press_action() { source_->press(); }
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/button/button.h"
|
||||
|
||||
namespace esphome::copy {
|
||||
|
||||
class CopyButton final : public button::Button, public Component {
|
||||
public:
|
||||
void set_source(button::Button *source) { source_ = source; }
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
|
||||
button::Button *source_;
|
||||
};
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -14,7 +14,8 @@ static constexpr float COVER_OPEN = 1.0f;
|
||||
static constexpr float COVER_CLOSED = 0.0f;
|
||||
|
||||
#define LOG_COVER(prefix, type, obj) \
|
||||
if (log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))) { \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
auto traits_ = (obj)->get_traits(); \
|
||||
if (traits_.get_is_assumed_state()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
|
||||
|
||||
@@ -117,7 +117,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))
|
||||
cg.add_optional(var.set_max_duration, config.get(CONF_MAX_DURATION))
|
||||
if (max_duration := config.get(CONF_MAX_DURATION)) is not None:
|
||||
cg.add(var.set_max_duration(max_duration))
|
||||
cg.add(var.set_malfunction_detection(config[CONF_MALFUNCTION_DETECTION]))
|
||||
if malfunction_action := config.get(CONF_MALFUNCTION_ACTION):
|
||||
await automation.build_automation(
|
||||
|
||||
@@ -9,14 +9,6 @@ ESPHOME_LOG_TAG(TAG, "daikin.climate");
|
||||
|
||||
static constexpr uint32_t BOOST_TIMEOUT_ID = 0;
|
||||
|
||||
static constexpr uint8_t DAIKIN_HEADER_BYTE0 = 0x11;
|
||||
static constexpr uint8_t DAIKIN_HEADER_BYTE1 = 0xDA;
|
||||
static constexpr uint8_t DAIKIN_HEADER_BYTE2 = 0x27;
|
||||
static constexpr uint8_t DAIKIN_HEADER_BYTE3 = 0x00;
|
||||
static constexpr uint8_t DAIKIN_FRAME_TYPE_INDEX = 4;
|
||||
static constexpr uint8_t DAIKIN_FRAME_HEADER_SIZE = DAIKIN_FRAME_TYPE_INDEX + 1;
|
||||
static constexpr uint8_t DAIKIN_STATE_FRAME_OFFSET = 2 * DAIKIN_COMFORT_FRAME_SIZE;
|
||||
|
||||
void DaikinClimate::setup() {
|
||||
climate_ir::ClimateIR::setup();
|
||||
// A boost preset restored from flash gets its timer too
|
||||
@@ -50,19 +42,37 @@ void DaikinClimate::transmit_state() {
|
||||
|
||||
data->mark(DAIKIN_HEADER_MARK);
|
||||
data->space(DAIKIN_HEADER_SPACE);
|
||||
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_state, DAIKIN_COMFORT_FRAME_SIZE);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
data->space(DAIKIN_MESSAGE_SPACE);
|
||||
data->mark(DAIKIN_HEADER_MARK);
|
||||
data->space(DAIKIN_HEADER_SPACE);
|
||||
|
||||
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_state + DAIKIN_COMFORT_FRAME_SIZE, DAIKIN_COMFORT_FRAME_SIZE);
|
||||
for (int i = 8; i < 16; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
data->space(DAIKIN_MESSAGE_SPACE);
|
||||
data->mark(DAIKIN_HEADER_MARK);
|
||||
data->space(DAIKIN_HEADER_SPACE);
|
||||
|
||||
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_state + DAIKIN_STATE_FRAME_OFFSET, DAIKIN_STATE_FRAME_SIZE);
|
||||
for (int i = 16; i < 35; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
@@ -274,24 +284,46 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
}
|
||||
|
||||
bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t frame[DAIKIN_STATE_FRAME_SIZE];
|
||||
uint8_t frame[DAIKIN_STATE_FRAME_SIZE] = {};
|
||||
if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Frame header, then the frame type that sets the frame size
|
||||
if (!data.expect_bytes<DAIKIN_BIT_TIMINGS>(frame, DAIKIN_FRAME_HEADER_SIZE) || frame[0] != DAIKIN_HEADER_BYTE0 ||
|
||||
frame[1] != DAIKIN_HEADER_BYTE1 || frame[2] != DAIKIN_HEADER_BYTE2 || frame[3] != DAIKIN_HEADER_BYTE3) {
|
||||
return false;
|
||||
}
|
||||
uint8_t frame_size = DAIKIN_STATE_FRAME_SIZE;
|
||||
if (frame[DAIKIN_FRAME_TYPE_INDEX] == DAIKIN_FRAME_TYPE_COMFORT) {
|
||||
frame_size = DAIKIN_COMFORT_FRAME_SIZE;
|
||||
} else if (frame[DAIKIN_FRAME_TYPE_INDEX] != DAIKIN_FRAME_TYPE_STATE) {
|
||||
return false;
|
||||
}
|
||||
if (!data.expect_bytes<DAIKIN_BIT_TIMINGS>(frame + DAIKIN_FRAME_HEADER_SIZE, frame_size - DAIKIN_FRAME_HEADER_SIZE)) {
|
||||
return false;
|
||||
for (uint8_t pos = 0; pos < frame_size; pos++) {
|
||||
uint8_t byte = 0;
|
||||
for (int8_t bit = 0; bit < 8; bit++) {
|
||||
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
|
||||
byte |= 1 << bit;
|
||||
} else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
frame[pos] = byte;
|
||||
if (pos == 0) {
|
||||
// frame header
|
||||
if (byte != 0x11)
|
||||
return false;
|
||||
} else if (pos == 1) {
|
||||
// frame header
|
||||
if (byte != 0xDA)
|
||||
return false;
|
||||
} else if (pos == 2) {
|
||||
// frame header
|
||||
if (byte != 0x27)
|
||||
return false;
|
||||
} else if (pos == 3) { // NOLINT(bugprone-branch-clone)
|
||||
// frame header
|
||||
if (byte != 0x00)
|
||||
return false;
|
||||
} else if (pos == 4) {
|
||||
// frame type
|
||||
if (byte == DAIKIN_FRAME_TYPE_COMFORT) {
|
||||
frame_size = DAIKIN_COMFORT_FRAME_SIZE;
|
||||
} else if (byte != DAIKIN_FRAME_TYPE_STATE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (frame[frame_size - 1] != sum_bytes(frame, frame_size - 1)) {
|
||||
return false;
|
||||
|
||||
@@ -45,8 +45,6 @@ const uint32_t DAIKIN_HEADER_SPACE = 1760;
|
||||
const uint32_t DAIKIN_BIT_MARK = 520;
|
||||
const uint32_t DAIKIN_ONE_SPACE = 1370;
|
||||
const uint32_t DAIKIN_ZERO_SPACE = 360;
|
||||
static constexpr remote_base::BitTimings DAIKIN_BIT_TIMINGS{
|
||||
.bit_mark = DAIKIN_BIT_MARK, .one_space = DAIKIN_ONE_SPACE, .zero_space = DAIKIN_ZERO_SPACE};
|
||||
const uint32_t DAIKIN_MESSAGE_SPACE = 32300;
|
||||
|
||||
// Frame types (byte 4) and sizes
|
||||
|
||||
@@ -39,7 +39,13 @@ void DaikinArcClimate::transmit_query_() {
|
||||
data->mark(DAIKIN_HEADER_MARK);
|
||||
data->space(DAIKIN_HEADER_SPACE);
|
||||
|
||||
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_header, sizeof(remote_header));
|
||||
for (uint8_t i : remote_header) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
bool bit = i & mask;
|
||||
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
@@ -113,14 +119,26 @@ void DaikinArcClimate::transmit_state() {
|
||||
data->mark(DAIKIN_HEADER_MARK);
|
||||
data->space(DAIKIN_HEADER_SPACE);
|
||||
|
||||
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_header, sizeof(remote_header));
|
||||
for (uint8_t i : remote_header) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
bool bit = i & mask;
|
||||
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
data->space(DAIKIN_MESSAGE_SPACE);
|
||||
|
||||
data->mark(DAIKIN_HEADER_MARK);
|
||||
data->space(DAIKIN_HEADER_SPACE);
|
||||
|
||||
data->item_bytes<DAIKIN_BIT_TIMINGS>(remote_state, sizeof(remote_state));
|
||||
for (uint8_t i : remote_state) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
bool bit = i & mask;
|
||||
data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DAIKIN_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
@@ -321,7 +339,7 @@ bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
}
|
||||
|
||||
bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE];
|
||||
uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {};
|
||||
|
||||
bool valid_daikin_frame = false;
|
||||
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
|
||||
@@ -400,11 +418,16 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
}
|
||||
|
||||
for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) {
|
||||
if (!data.expect_bytes<DAIKIN_BIT_TIMINGS>(&state_frame[pos], 1)) {
|
||||
ESP_LOGI(TAG, "non daikin_arc expect item pos: %d", pos);
|
||||
return false;
|
||||
uint8_t byte = 0;
|
||||
for (int8_t bit = 0; bit < 8; bit++) {
|
||||
if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) {
|
||||
byte |= 1 << bit;
|
||||
} else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) {
|
||||
ESP_LOGI(TAG, "non daikin_arc expect item pos: %d", pos);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const uint8_t byte = state_frame[pos];
|
||||
state_frame[pos] = byte;
|
||||
if (pos == 0) {
|
||||
// frame header
|
||||
if (byte != 0x11) {
|
||||
|
||||
@@ -36,8 +36,6 @@ const uint32_t DAIKIN_HEADER_SPACE = 1760;
|
||||
const uint32_t DAIKIN_BIT_MARK = 400;
|
||||
const uint32_t DAIKIN_ONE_SPACE = 1300;
|
||||
const uint32_t DAIKIN_ZERO_SPACE = 480;
|
||||
static constexpr remote_base::BitTimings DAIKIN_BIT_TIMINGS{
|
||||
.bit_mark = DAIKIN_BIT_MARK, .one_space = DAIKIN_ONE_SPACE, .zero_space = DAIKIN_ZERO_SPACE};
|
||||
const uint32_t DAIKIN_MESSAGE_SPACE = 35000;
|
||||
|
||||
const uint8_t DAIKIN_DBG_TOLERANCE = 25;
|
||||
|
||||
@@ -6,12 +6,6 @@ namespace esphome::daikin_brc {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "daikin_brc.climate");
|
||||
|
||||
static constexpr uint8_t DAIKIN_BRC_HEADER_BYTE0 = 0x11;
|
||||
static constexpr uint8_t DAIKIN_BRC_HEADER_BYTE1 = 0xDA;
|
||||
static constexpr uint8_t DAIKIN_BRC_HEADER_BYTE2 = 0x17;
|
||||
static constexpr uint8_t DAIKIN_BRC_HEADER_BYTE3 = 0x18;
|
||||
static constexpr uint8_t DAIKIN_BRC_FRAME_TYPE_STATE = 0x00;
|
||||
|
||||
void DaikinBrcClimate::control(const climate::ClimateCall &call) {
|
||||
this->mode_button_ = 0x00;
|
||||
if (call.get_mode().has_value()) {
|
||||
@@ -48,14 +42,25 @@ void DaikinBrcClimate::transmit_state() {
|
||||
|
||||
data->mark(DAIKIN_BRC_HEADER_MARK);
|
||||
data->space(DAIKIN_BRC_HEADER_SPACE);
|
||||
data->item_bytes<DAIKIN_BRC_BIT_TIMINGS>(remote_state, DAIKIN_BRC_PREAMBLE_SIZE);
|
||||
for (int i = 0; i < DAIKIN_BRC_PREAMBLE_SIZE; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DAIKIN_BRC_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DAIKIN_BRC_BIT_MARK);
|
||||
data->space(DAIKIN_BRC_MESSAGE_SPACE);
|
||||
data->mark(DAIKIN_BRC_HEADER_MARK);
|
||||
data->space(DAIKIN_BRC_HEADER_SPACE);
|
||||
|
||||
data->item_bytes<DAIKIN_BRC_BIT_TIMINGS>(remote_state + DAIKIN_BRC_PREAMBLE_SIZE,
|
||||
DAIKIN_BRC_TRANSMIT_FRAME_SIZE - DAIKIN_BRC_PREAMBLE_SIZE);
|
||||
for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DAIKIN_BRC_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
data->mark(DAIKIN_BRC_BIT_MARK);
|
||||
data->space(0);
|
||||
@@ -227,16 +232,41 @@ bool DaikinBrcClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
}
|
||||
|
||||
bool DaikinBrcClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE];
|
||||
uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE] = {};
|
||||
if (!data.expect_item(DAIKIN_BRC_HEADER_MARK, DAIKIN_BRC_HEADER_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
// Frame header, with this unit's address in the low 3 bits of byte 3, then the frame type
|
||||
if (!data.expect_bytes<DAIKIN_BRC_BIT_TIMINGS>(state_frame, DAIKIN_BRC_STATE_FRAME_SIZE) ||
|
||||
state_frame[0] != DAIKIN_BRC_HEADER_BYTE0 || state_frame[1] != DAIKIN_BRC_HEADER_BYTE1 ||
|
||||
state_frame[2] != DAIKIN_BRC_HEADER_BYTE2 || state_frame[3] != (DAIKIN_BRC_HEADER_BYTE3 | this->sub_unit_) ||
|
||||
state_frame[4] != DAIKIN_BRC_FRAME_TYPE_STATE) {
|
||||
return false;
|
||||
for (uint8_t pos = 0; pos < DAIKIN_BRC_STATE_FRAME_SIZE; pos++) {
|
||||
uint8_t byte = 0;
|
||||
for (int8_t bit = 0; bit < 8; bit++) {
|
||||
if (data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ONE_SPACE)) {
|
||||
byte |= 1 << bit;
|
||||
} else if (!data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state_frame[pos] = byte;
|
||||
if (pos == 0) {
|
||||
// frame header
|
||||
if (byte != 0x11)
|
||||
return false;
|
||||
} else if (pos == 1) {
|
||||
// frame header
|
||||
if (byte != 0xDA)
|
||||
return false;
|
||||
} else if (pos == 2) {
|
||||
// frame header
|
||||
if (byte != 0x17)
|
||||
return false;
|
||||
} else if (pos == 3) {
|
||||
// frame header, with this unit's address in the low 3 bits
|
||||
if (byte != (0x18 | this->sub_unit_))
|
||||
return false;
|
||||
} else if (pos == 4) {
|
||||
// frame type
|
||||
if (byte != 0x00)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this->parse_state_frame_(state_frame);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ const uint32_t DAIKIN_BRC_IR_FREQUENCY = 38000;
|
||||
const uint32_t DAIKIN_BRC_HEADER_MARK = 5070;
|
||||
const uint32_t DAIKIN_BRC_HEADER_SPACE = 2140;
|
||||
const uint32_t DAIKIN_BRC_BIT_MARK = 370;
|
||||
static constexpr remote_base::BitTimings DAIKIN_BRC_BIT_TIMINGS{
|
||||
.bit_mark = DAIKIN_BRC_BIT_MARK, .one_space = 1780, .zero_space = 710};
|
||||
const uint32_t DAIKIN_BRC_ONE_SPACE = 1780;
|
||||
const uint32_t DAIKIN_BRC_ZERO_SPACE = 710;
|
||||
const uint32_t DAIKIN_BRC_MESSAGE_SPACE = 29410;
|
||||
|
||||
const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_F = 72; // Dry/Fan mode is always 17 Celsius.
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
namespace esphome::datetime {
|
||||
|
||||
#define LOG_DATETIME_DATE(prefix, type, obj) \
|
||||
if (log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))) { \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
namespace esphome::datetime {
|
||||
|
||||
#define LOG_DATETIME_DATETIME(prefix, type, obj) \
|
||||
if (log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))) { \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
namespace esphome::datetime {
|
||||
|
||||
#define LOG_DATETIME_TIME(prefix, type, obj) \
|
||||
if (log_entity_header(TAG, prefix, LOG_STR_LITERAL(type), (obj))) { \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,10 @@ _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:
|
||||
@@ -352,7 +356,6 @@ 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,
|
||||
)
|
||||
|
||||
@@ -361,7 +364,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add_optional(var.set_sleep_duration, config.get(CONF_SLEEP_DURATION))
|
||||
if CONF_SLEEP_DURATION in config:
|
||||
cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION]))
|
||||
if (wakeup_pins := config.get(CONF_WAKEUP_PIN)) is not None:
|
||||
if CORE.is_bk72xx:
|
||||
cg.add(var.init_wakeup_pins(len(wakeup_pins)))
|
||||
@@ -376,7 +380,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
else:
|
||||
await cg.add_pin(var.set_wakeup_pin, wakeup_pins[0][CONF_PIN])
|
||||
cg.add_optional(var.set_wakeup_pin_mode, config.get(CONF_WAKEUP_PIN_MODE))
|
||||
if CONF_WAKEUP_PIN_MODE in config:
|
||||
cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE]))
|
||||
if CONF_RUN_DURATION in config:
|
||||
run_duration_config = config[CONF_RUN_DURATION]
|
||||
if not isinstance(run_duration_config, dict):
|
||||
@@ -422,7 +427,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
await automation.build_automation(trigger, [(WakeupCause, "cause")], wake_conf)
|
||||
cg.add_define("USE_DEEP_SLEEP_ON_WAKE")
|
||||
|
||||
cg.add_optional(var.set_touch_wakeup, config.get(CONF_TOUCH_WAKEUP))
|
||||
if CONF_TOUCH_WAKEUP in config:
|
||||
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
|
||||
if CORE.using_zephyr:
|
||||
# Devices are suspended when CPU is entering a low power state
|
||||
# https://github.com/nrfconnect/sdk-zephyr/blob/v3.7.99-ncs2-2/doc/services/pm/device.rst#system-managed-device-power-management
|
||||
|
||||
@@ -23,7 +23,13 @@ void DelonghiClimate::transmit_state() {
|
||||
|
||||
data->mark(DELONGHI_HEADER_MARK);
|
||||
data->space(DELONGHI_HEADER_SPACE);
|
||||
data->item_bytes<DELONGHI_BIT_TIMINGS>(remote_state, DELONGHI_STATE_FRAME_SIZE);
|
||||
for (unsigned char b : remote_state) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DELONGHI_BIT_MARK);
|
||||
bool bit = b & mask;
|
||||
data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DELONGHI_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
@@ -149,13 +155,27 @@ bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
}
|
||||
|
||||
bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE];
|
||||
uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {};
|
||||
if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
if (!data.expect_bytes<DELONGHI_BIT_TIMINGS>(state_frame, DELONGHI_STATE_FRAME_SIZE) ||
|
||||
state_frame[0] != DELONGHI_ADDRESS)
|
||||
return false;
|
||||
for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) {
|
||||
uint8_t byte = 0;
|
||||
for (int8_t bit = 0; bit < 8; bit++) {
|
||||
if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) {
|
||||
byte |= 1 << bit;
|
||||
} else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state_frame[pos] = byte;
|
||||
if (pos == 0) {
|
||||
// frame header
|
||||
if (byte != DELONGHI_ADDRESS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this->parse_state_frame_(state_frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ const uint32_t DELONGHI_IR_FREQUENCY = 38000;
|
||||
const uint32_t DELONGHI_HEADER_MARK = 9000;
|
||||
const uint32_t DELONGHI_HEADER_SPACE = 4500;
|
||||
const uint32_t DELONGHI_BIT_MARK = 465;
|
||||
static constexpr remote_base::BitTimings DELONGHI_BIT_TIMINGS{
|
||||
.bit_mark = DELONGHI_BIT_MARK, .one_space = 1750, .zero_space = 670};
|
||||
const uint32_t DELONGHI_ONE_SPACE = 1750;
|
||||
const uint32_t DELONGHI_ZERO_SPACE = 670;
|
||||
|
||||
// State Frame size
|
||||
const uint8_t DELONGHI_STATE_FRAME_SIZE = 8;
|
||||
|
||||
@@ -42,6 +42,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_LINE_READER")
|
||||
await uart.new_uart_component(config)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
@@ -27,9 +29,12 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
|
||||
TEMPLATABLE_VALUE(float, det_max4)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(0));
|
||||
// Worst case: stop, factory reset, six settings, save, start
|
||||
if (!this->parent_->has_queue_room(10))
|
||||
return;
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, false);
|
||||
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
|
||||
this->parent_->enqueue(make_unique<FactoryResetCommand>());
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_FACTORY_RESET);
|
||||
}
|
||||
if (this->det_min1_.has_value() && this->det_max1_.has_value()) {
|
||||
// Each segment is read once, so the guard and the command see the same values.
|
||||
@@ -37,10 +42,10 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
|
||||
if (min1 >= 0) {
|
||||
const float max1 = this->det_max1_.value(x...);
|
||||
if (max1 >= 0) {
|
||||
this->parent_->enqueue(make_unique<DetRangeCfgCommand>(
|
||||
min1, max1, this->det_min2_.value_or(x..., -1), this->det_max2_.value_or(x..., -1),
|
||||
this->det_min3_.value_or(x..., -1), this->det_max3_.value_or(x..., -1),
|
||||
this->det_min4_.value_or(x..., -1), this->det_max4_.value_or(x..., -1)));
|
||||
this->parent_->enqueue_det_range_cfg(min1, max1, this->det_min2_.value_or(x..., -1),
|
||||
this->det_max2_.value_or(x..., -1), this->det_min3_.value_or(x..., -1),
|
||||
this->det_max3_.value_or(x..., -1), this->det_min4_.value_or(x..., -1),
|
||||
this->det_max4_.value_or(x..., -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,38 +53,35 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
|
||||
float detect = this->delay_after_detect_.value(x...);
|
||||
float disappear = this->delay_after_disappear_.value(x...);
|
||||
if (detect >= 0 && disappear >= 0) {
|
||||
this->parent_->enqueue(make_unique<SetLatencyCommand>(detect, disappear));
|
||||
this->parent_->enqueue_set_latency(detect, disappear);
|
||||
}
|
||||
}
|
||||
if (this->start_after_power_on_.has_value()) {
|
||||
int8_t val = this->start_after_power_on_.value(x...);
|
||||
if (val >= 0) {
|
||||
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(val));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSOR_CFG_START, val);
|
||||
}
|
||||
}
|
||||
if (this->turn_on_led_.has_value()) {
|
||||
int8_t val = this->turn_on_led_.value(x...);
|
||||
if (val >= 0) {
|
||||
this->parent_->enqueue(make_unique<LedModeCommand>(val));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_LED_MODE, val);
|
||||
}
|
||||
}
|
||||
if (this->presence_via_uart_.has_value()) {
|
||||
int8_t val = this->presence_via_uart_.value(x...);
|
||||
if (val >= 0) {
|
||||
this->parent_->enqueue(make_unique<UartOutputCommand>(val));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_UART_OUTPUT, val);
|
||||
}
|
||||
}
|
||||
if (this->sensitivity_.has_value()) {
|
||||
int8_t val = this->sensitivity_.value(x...);
|
||||
if (val >= 0) {
|
||||
if (val > 9) {
|
||||
val = 9;
|
||||
}
|
||||
this->parent_->enqueue(make_unique<SensitivityCommand>(val));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSITIVITY, std::min<int8_t>(val, 9));
|
||||
}
|
||||
}
|
||||
this->parent_->enqueue(make_unique<SaveCfgCommand>());
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(1));
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
|
||||
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
#include "commands.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "dfrobot_sen0395.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395.commands");
|
||||
|
||||
uint8_t Command::execute(DfrobotSen0395Component *parent) {
|
||||
this->parent_ = parent;
|
||||
if (this->cmd_sent_) {
|
||||
if (this->parent_->read_message_()) {
|
||||
std::string message(this->parent_->read_buffer_);
|
||||
if (message.rfind("is not recognized as a CLI command") != std::string::npos) {
|
||||
ESP_LOGD(TAG, "Command not recognized properly by sensor");
|
||||
if (this->retries_left_ > 0) {
|
||||
this->retries_left_ -= 1;
|
||||
this->cmd_sent_ = false;
|
||||
ESP_LOGD(TAG, "Retrying");
|
||||
return 0;
|
||||
} else {
|
||||
this->parent_->find_prompt_();
|
||||
return 1; // Command done
|
||||
}
|
||||
}
|
||||
uint8_t rc = on_message(message);
|
||||
if (rc == 2) {
|
||||
if (this->retries_left_ > 0) {
|
||||
this->retries_left_ -= 1;
|
||||
this->cmd_sent_ = false;
|
||||
ESP_LOGD(TAG, "Retrying");
|
||||
return 0;
|
||||
} else {
|
||||
this->parent_->find_prompt_();
|
||||
return 1; // Command done
|
||||
}
|
||||
} else if (rc == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
this->parent_->find_prompt_();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
|
||||
ESP_LOGD(TAG, "Command timeout");
|
||||
if (this->retries_left_ > 0) {
|
||||
this->retries_left_ -= 1;
|
||||
this->cmd_sent_ = false;
|
||||
ESP_LOGD(TAG, "Retrying");
|
||||
} else {
|
||||
return 1; // Command done
|
||||
}
|
||||
}
|
||||
} else if (this->parent_->send_cmd_(this->cmd_.c_str(), this->cmd_duration_ms_)) {
|
||||
this->cmd_sent_ = true;
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t ReadStateCommand::execute(DfrobotSen0395Component *parent) {
|
||||
this->parent_ = parent;
|
||||
if (this->parent_->read_message_()) {
|
||||
std::string message(this->parent_->read_buffer_);
|
||||
if (message.rfind("$JYBSS,0, , , *") != std::string::npos) {
|
||||
this->parent_->set_detected_(false);
|
||||
this->parent_->set_active(true);
|
||||
return 1; // Command done
|
||||
} else if (message.rfind("$JYBSS,1, , , *") != std::string::npos) {
|
||||
this->parent_->set_detected_(true);
|
||||
this->parent_->set_active(true);
|
||||
return 1; // Command done
|
||||
}
|
||||
}
|
||||
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
|
||||
return 1; // Command done, timeout
|
||||
}
|
||||
return 0; // Command not done yet.
|
||||
}
|
||||
|
||||
uint8_t ReadStateCommand::on_message(std::string &message) { return 1; }
|
||||
|
||||
uint8_t PowerCommand::on_message(std::string &message) {
|
||||
if (message == "sensor stopped already") {
|
||||
this->parent_->set_active(false);
|
||||
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
|
||||
return 1; // Command done
|
||||
} else if (message == "sensor started already") {
|
||||
this->parent_->set_active(true);
|
||||
ESP_LOGI(TAG, "Started sensor (already started)");
|
||||
return 1; // Command done
|
||||
} else if (message == "new parameter isn't save, can't startSensor") {
|
||||
this->parent_->set_active(false);
|
||||
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
this->parent_->set_active(this->power_on_);
|
||||
if (this->power_on_) {
|
||||
ESP_LOGI(TAG, "Started sensor");
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Stopped sensor");
|
||||
}
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet.
|
||||
}
|
||||
|
||||
DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3,
|
||||
float min4, float max4) {
|
||||
// TODO: Print warning when values are rounded
|
||||
if (min1 < 0 || max1 < 0) {
|
||||
this->min1_ = min1 = 0;
|
||||
this->max1_ = max1 = 0;
|
||||
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
|
||||
this->max4_ = max4 = -1;
|
||||
|
||||
ESP_LOGW(TAG, "DetRangeCfgCommand invalid input parameters. Using range config 0 0.");
|
||||
|
||||
this->cmd_ = "detRangeCfg -1 0 0";
|
||||
} else if (min2 < 0 || max2 < 0) {
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
|
||||
this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15f, max1 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else if (min3 < 0 || max3 < 0) {
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
|
||||
max2 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else if (min4 < 0 || max4 < 0) {
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
|
||||
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
|
||||
this->min4_ = min4 = this->max4_ = max4 = -1;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
|
||||
max2 / 0.15f, min3 / 0.15f, max3 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
} else {
|
||||
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
|
||||
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
|
||||
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
|
||||
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
|
||||
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
|
||||
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
|
||||
this->min4_ = min4 = roundf(min4 / 0.15f) * 0.15f;
|
||||
this->max4_ = max4 = roundf(max4 / 0.15f) * 0.15f;
|
||||
|
||||
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
|
||||
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f,
|
||||
min2 / 0.15f, max2 / 0.15f, min3 / 0.15f, max3 / 0.15f, min4 / 0.15f, max4 / 0.15f);
|
||||
this->cmd_ = buf;
|
||||
}
|
||||
|
||||
this->min1_ = min1;
|
||||
this->max1_ = max1;
|
||||
this->min2_ = min2;
|
||||
this->max2_ = max2;
|
||||
this->min3_ = min3;
|
||||
this->max3_ = max3;
|
||||
this->min4_ = min4;
|
||||
this->max4_ = max4;
|
||||
};
|
||||
|
||||
uint8_t DetRangeCfgCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot configure range config. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG,
|
||||
"Updated detection area config:\n"
|
||||
" Detection area 1 from %.02fm to %.02fm.",
|
||||
this->min1_, this->max1_);
|
||||
if (this->min2_ >= 0 && this->max2_ >= 0) {
|
||||
ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_);
|
||||
}
|
||||
if (this->min3_ >= 0 && this->max3_ >= 0) {
|
||||
ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_);
|
||||
}
|
||||
if (this->min4_ >= 0 && this->max4_ >= 0) {
|
||||
ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_);
|
||||
}
|
||||
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet.
|
||||
}
|
||||
|
||||
SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) {
|
||||
delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f;
|
||||
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
|
||||
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
|
||||
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
|
||||
// max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
|
||||
this->cmd_ = buf;
|
||||
};
|
||||
|
||||
uint8_t SetLatencyCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG,
|
||||
"Updated output latency config:\n"
|
||||
" Signal that someone was detected is delayed by %.03f s.\n"
|
||||
" Signal that nobody is detected anymore is delayed by %.03f s.",
|
||||
this->delay_after_detection_, this->delay_after_disappear_);
|
||||
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t SensorCfgStartCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot configure sensor startup behavior. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Updated sensor startup behavior:");
|
||||
if (startup_mode_) {
|
||||
this->parent_->set_start_after_boot(true);
|
||||
ESP_LOGI(TAG, "Sensor will start automatically after power-on.");
|
||||
} else {
|
||||
this->parent_->set_start_after_boot(false);
|
||||
ESP_LOGI(TAG, "Sensor needs to be started manually after power-on.");
|
||||
}
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t FactoryResetCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot factory reset. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Sensor factory reset done.");
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t ResetSystemCommand::on_message(std::string &message) {
|
||||
if (message == "leapMMW:/>") {
|
||||
ESP_LOGI(TAG, "Restarted sensor.");
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t SaveCfgCommand::on_message(std::string &message) {
|
||||
if (message == "no parameter has changed") {
|
||||
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t LedModeCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot set led mode. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Set led mode done.");
|
||||
if (this->active_) {
|
||||
this->parent_->set_led_active(true);
|
||||
ESP_LOGI(TAG, "Sensor LED will blink.");
|
||||
} else {
|
||||
this->parent_->set_led_active(false);
|
||||
ESP_LOGI(TAG, "Turned off LED.");
|
||||
}
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t UartOutputCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot set uart output mode. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Set uart mode done.");
|
||||
if (this->active_) {
|
||||
this->parent_->set_uart_presence_active(true);
|
||||
ESP_LOGI(TAG, "Presence information is sent via UART and GPIO.");
|
||||
} else {
|
||||
this->parent_->set_uart_presence_active(false);
|
||||
ESP_LOGI(TAG, "Presence information is only sent via GPIO.");
|
||||
}
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
uint8_t SensitivityCommand::on_message(std::string &message) {
|
||||
if (message == "sensor is not stopped") {
|
||||
ESP_LOGE(TAG, "Cannot set sensitivity. Sensor is not stopped!");
|
||||
return 1; // Command done
|
||||
} else if (message == "Done") {
|
||||
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", this->sensitivity_);
|
||||
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
|
||||
return 1; // Command done
|
||||
}
|
||||
return 0; // Command not done yet
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
@@ -1,152 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
class DfrobotSen0395Component;
|
||||
|
||||
// Use command queue and time stamps to avoid blocking.
|
||||
// When component has run time, check if minimum time (1s) between
|
||||
// commands has passed. After that run a command from the queue.
|
||||
class Command {
|
||||
public:
|
||||
virtual ~Command() = default;
|
||||
virtual uint8_t execute(DfrobotSen0395Component *parent);
|
||||
virtual uint8_t on_message(std::string &message) = 0;
|
||||
|
||||
protected:
|
||||
DfrobotSen0395Component *parent_{nullptr};
|
||||
std::string cmd_;
|
||||
bool cmd_sent_{false};
|
||||
int8_t retries_left_{2};
|
||||
uint32_t cmd_duration_ms_{1000};
|
||||
uint32_t timeout_ms_{1500};
|
||||
};
|
||||
|
||||
class ReadStateCommand : public Command {
|
||||
public:
|
||||
ReadStateCommand() { timeout_ms_ = 500; }
|
||||
uint8_t execute(DfrobotSen0395Component *parent) override;
|
||||
uint8_t on_message(std::string &message) override;
|
||||
};
|
||||
|
||||
class PowerCommand : public Command {
|
||||
public:
|
||||
PowerCommand(bool power_on) : power_on_(power_on) {
|
||||
if (power_on) {
|
||||
cmd_ = "sensorStart";
|
||||
} else {
|
||||
cmd_ = "sensorStop";
|
||||
}
|
||||
};
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
bool power_on_;
|
||||
};
|
||||
|
||||
class DetRangeCfgCommand : public Command {
|
||||
public:
|
||||
DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, float min4, float max4);
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
float min1_, max1_, min2_, max2_, min3_, max3_, min4_, max4_;
|
||||
// TODO: Set min max values in component, so they can be published as sensor.
|
||||
};
|
||||
|
||||
class SetLatencyCommand : public Command {
|
||||
public:
|
||||
SetLatencyCommand(float delay_after_detection, float delay_after_disappear);
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
float delay_after_detection_;
|
||||
float delay_after_disappear_;
|
||||
};
|
||||
|
||||
class SensorCfgStartCommand : public Command {
|
||||
public:
|
||||
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
|
||||
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
|
||||
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
|
||||
cmd_ = std::string(tmp_cmd);
|
||||
}
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
bool startup_mode_;
|
||||
};
|
||||
|
||||
class FactoryResetCommand : public Command {
|
||||
public:
|
||||
FactoryResetCommand() { cmd_ = "factoryReset 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; };
|
||||
uint8_t on_message(std::string &message) override;
|
||||
};
|
||||
|
||||
class ResetSystemCommand : public Command {
|
||||
public:
|
||||
ResetSystemCommand() { cmd_ = "resetSystem"; }
|
||||
uint8_t on_message(std::string &message) override;
|
||||
};
|
||||
|
||||
class SaveCfgCommand : public Command {
|
||||
public:
|
||||
SaveCfgCommand() {
|
||||
cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89";
|
||||
cmd_duration_ms_ = 3000;
|
||||
timeout_ms_ = 3500;
|
||||
}
|
||||
uint8_t on_message(std::string &message) override;
|
||||
};
|
||||
|
||||
class LedModeCommand : public Command {
|
||||
public:
|
||||
LedModeCommand(bool active) : active_(active) {
|
||||
if (active) {
|
||||
cmd_ = "setLedMode 1 0";
|
||||
} else {
|
||||
cmd_ = "setLedMode 1 1";
|
||||
}
|
||||
};
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
bool active_;
|
||||
};
|
||||
|
||||
class UartOutputCommand : public Command {
|
||||
public:
|
||||
UartOutputCommand(bool active) : active_(active) {
|
||||
if (active) {
|
||||
cmd_ = "setUartOutput 1 1";
|
||||
} else {
|
||||
cmd_ = "setUartOutput 1 0";
|
||||
}
|
||||
};
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
bool active_;
|
||||
};
|
||||
|
||||
class SensitivityCommand : public Command {
|
||||
public:
|
||||
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
|
||||
if (sensitivity > 9)
|
||||
sensitivity_ = sensitivity = 9;
|
||||
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
|
||||
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
|
||||
cmd_ = std::string(tmp_cmd);
|
||||
};
|
||||
uint8_t on_message(std::string &message) override;
|
||||
|
||||
protected:
|
||||
uint8_t sensitivity_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
@@ -1,13 +1,27 @@
|
||||
#include "dfrobot_sen0395.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395");
|
||||
const char ASCII_CR = 0x0D;
|
||||
const char ASCII_LF = 0x0A;
|
||||
static constexpr float DET_RANGE_STEP_M = 0.15f;
|
||||
static constexpr float LATENCY_STEP_S = 0.025f;
|
||||
// Sent after a reset with no line end
|
||||
static constexpr char PROMPT[] PROGMEM = "leapMMW:/>";
|
||||
|
||||
// What each CommandType cannot do while the sensor runs; empty for commands that do not need it stopped
|
||||
PROGMEM_STRING_TABLE(BusyActions, "", "configure range config", "configure output latency",
|
||||
"configure sensor startup behavior", "factory reset", "", "", "set led mode",
|
||||
"set uart output mode", "set sensitivity");
|
||||
static_assert(BusyActions::COUNT == static_cast<size_t>(CommandType::COMMAND_TYPE_SENSITIVITY) + 1,
|
||||
"BusyActions needs one entry per CommandType");
|
||||
|
||||
void DfrobotSen0395Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Dfrobot Mmwave Radar:");
|
||||
@@ -23,72 +37,287 @@ void DfrobotSen0395Component::dump_config() {
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::loop() {
|
||||
if (cmd_queue_.is_empty()) {
|
||||
// Command queue empty. Read sensor state.
|
||||
cmd_queue_.enqueue(make_unique<ReadStateCommand>());
|
||||
if (this->cmd_queue_.empty()) {
|
||||
this->read_state_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Commands are non-blocking and need to be called repeatedly.
|
||||
if (cmd_queue_.process(this)) {
|
||||
// Dequeue if command is done
|
||||
cmd_queue_.dequeue();
|
||||
// Commands are non-blocking and need to be called repeatedly
|
||||
if (this->run_command_(this->cmd_queue_.front(), App.get_loop_component_start_time())) {
|
||||
this->cmd_queue_.pop();
|
||||
this->cmd_sent_ = false;
|
||||
this->retries_left_ = 2;
|
||||
}
|
||||
}
|
||||
|
||||
int8_t DfrobotSen0395Component::enqueue(std::unique_ptr<Command> cmd) {
|
||||
return cmd_queue_.enqueue(std::move(cmd)); // Transfer ownership using std::move
|
||||
bool DfrobotSen0395Component::enqueue(CommandType type, uint8_t arg) {
|
||||
if (!this->cmd_queue_.push({type, arg})) {
|
||||
ESP_LOGE(TAG, "Command queue is full");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t DfrobotSen0395Component::read_message_() {
|
||||
bool DfrobotSen0395Component::has_queue_room(size_t count) const {
|
||||
if (COMMAND_QUEUE_SIZE - this->cmd_queue_.size() >= count)
|
||||
return true;
|
||||
ESP_LOGE(TAG, "Command queue is full");
|
||||
return false;
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::enqueue_det_range_cfg(float min1, float max1, float min2, float max2, float min3,
|
||||
float max3, float min4, float max4) {
|
||||
// TODO: Print warning when values are rounded
|
||||
const float values[8] = {min1, max1, min2, max2, min3, max3, min4, max4};
|
||||
int16_t steps[8];
|
||||
bool in_use = true;
|
||||
for (size_t i = 0; i < 8; i += 2) {
|
||||
in_use = in_use && values[i] >= 0 && values[i + 1] >= 0;
|
||||
for (size_t j = i; j < i + 2; j++) {
|
||||
steps[j] = in_use ? static_cast<int16_t>(clamp(roundf(values[j] / DET_RANGE_STEP_M), 0.0f, 32767.0f)) : -1;
|
||||
}
|
||||
}
|
||||
if (steps[0] < 0) {
|
||||
ESP_LOGW(TAG, "Invalid detection range, using 0 0");
|
||||
steps[0] = steps[1] = 0;
|
||||
}
|
||||
if (this->enqueue(CommandType::COMMAND_TYPE_DET_RANGE_CFG))
|
||||
memcpy(this->det_range_steps_, steps, sizeof(steps));
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::enqueue_set_latency(float delay_after_detection, float delay_after_disappear) {
|
||||
if (!this->enqueue(CommandType::COMMAND_TYPE_SET_LATENCY))
|
||||
return;
|
||||
// 1638.375 s is 65535 steps
|
||||
this->latency_steps_[0] =
|
||||
static_cast<uint16_t>(clamp(std::round(delay_after_detection / LATENCY_STEP_S), 0.0f, 65535.0f));
|
||||
this->latency_steps_[1] =
|
||||
static_cast<uint16_t>(clamp(std::round(delay_after_disappear / LATENCY_STEP_S), 0.0f, 65535.0f));
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::enqueue_saved_setting(CommandType type, bool value) {
|
||||
const bool was_active = this->active_;
|
||||
if (!this->has_queue_room(was_active ? 4 : 2))
|
||||
return;
|
||||
if (was_active)
|
||||
this->enqueue(CommandType::COMMAND_TYPE_POWER, false);
|
||||
this->enqueue(type, value);
|
||||
this->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
|
||||
if (was_active)
|
||||
this->enqueue(CommandType::COMMAND_TYPE_POWER, true);
|
||||
}
|
||||
|
||||
bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
|
||||
// The interval between two commands must be larger than the command duration
|
||||
const uint32_t duration = cmd.type == CommandType::COMMAND_TYPE_SAVE_CFG ? 3000 : 1000;
|
||||
if (!this->cmd_sent_) {
|
||||
if (now - this->ts_last_cmd_sent_ > duration) {
|
||||
char buf[64]; // "detRangeCfg -1" + 8 * " 32767" + null
|
||||
this->format_command(cmd, buf, sizeof(buf));
|
||||
ESP_LOGD(TAG, "Sending command: %s", buf);
|
||||
this->write_str(buf);
|
||||
this->ts_last_cmd_sent_ = now;
|
||||
this->cmd_sent_ = true;
|
||||
}
|
||||
return false; // Command not done yet
|
||||
}
|
||||
|
||||
if (const char *message = this->read_message_()) {
|
||||
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
|
||||
if (!this->handle_reply_(cmd, message))
|
||||
return false; // Command not done yet
|
||||
this->read_message_(); // Skip the prompt
|
||||
return true;
|
||||
}
|
||||
ESP_LOGD(TAG, "Command not recognized properly by sensor");
|
||||
} else if (now - this->ts_last_cmd_sent_ <= duration + 500) {
|
||||
return false; // Command not done yet
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Command timeout");
|
||||
}
|
||||
|
||||
if (this->retries_left_ == 0) {
|
||||
this->read_message_(); // Skip the prompt
|
||||
return true; // Give up
|
||||
}
|
||||
this->retries_left_--;
|
||||
this->cmd_sent_ = false;
|
||||
ESP_LOGD(TAG, "Retrying");
|
||||
return false;
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::read_state_() {
|
||||
const char *message = this->read_message_();
|
||||
if (message == nullptr)
|
||||
return;
|
||||
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
|
||||
this->set_detected_(false);
|
||||
this->set_active(true);
|
||||
} else if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
|
||||
this->set_detected_(true);
|
||||
this->set_active(true);
|
||||
}
|
||||
}
|
||||
|
||||
static size_t append_cfg_key(char *buf, size_t size, size_t pos) {
|
||||
return buf_append_str(buf, size, pos, " 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89");
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::format_command(const Command &cmd, char *buf, size_t size) const {
|
||||
buf[0] = '\0';
|
||||
switch (cmd.type) {
|
||||
case CommandType::COMMAND_TYPE_POWER:
|
||||
if (cmd.arg) {
|
||||
buf_append_str(buf, size, 0, "sensorStart");
|
||||
} else {
|
||||
buf_append_str(buf, size, 0, "sensorStop");
|
||||
}
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_DET_RANGE_CFG: {
|
||||
size_t pos = buf_append_str(buf, size, 0, "detRangeCfg -1");
|
||||
for (size_t i = 0; i < 8 && this->det_range_steps_[i] >= 0; i += 2) {
|
||||
pos = buf_append_printf(buf, size, pos, " %d %d", this->det_range_steps_[i], this->det_range_steps_[i + 1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CommandType::COMMAND_TYPE_SET_LATENCY:
|
||||
buf_append_printf(buf, size, 0, "setLatency %.03f %.03f", this->latency_steps_[0] * LATENCY_STEP_S,
|
||||
this->latency_steps_[1] * LATENCY_STEP_S);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SENSOR_CFG_START:
|
||||
buf_append_printf(buf, size, 0, "sensorCfgStart %d", cmd.arg);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_FACTORY_RESET:
|
||||
append_cfg_key(buf, size, buf_append_str(buf, size, 0, "factoryReset"));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
|
||||
buf_append_str(buf, size, 0, "resetSystem");
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SAVE_CFG:
|
||||
append_cfg_key(buf, size, buf_append_str(buf, size, 0, "saveCfg"));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_LED_MODE:
|
||||
buf_append_printf(buf, size, 0, "setLedMode 1 %d", cmd.arg ? 0 : 1);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_UART_OUTPUT:
|
||||
buf_append_printf(buf, size, 0, "setUartOutput 1 %d", cmd.arg ? 1 : 0);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SENSITIVITY:
|
||||
buf_append_printf(buf, size, 0, "setSensitivity %d", cmd.arg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool DfrobotSen0395Component::handle_reply_(const Command &cmd, const char *message) {
|
||||
auto is = [message](ESPHOME_PGM_P text) { return ESPHOME_strcmp_P(message, text) == 0; };
|
||||
|
||||
// Commands with their own replies
|
||||
switch (cmd.type) {
|
||||
case CommandType::COMMAND_TYPE_POWER:
|
||||
if (is(ESPHOME_PSTR("sensor stopped already"))) {
|
||||
this->set_active(false);
|
||||
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
|
||||
} else if (is(ESPHOME_PSTR("sensor started already"))) {
|
||||
this->set_active(true);
|
||||
ESP_LOGI(TAG, "Started sensor (already started)");
|
||||
} else if (is(ESPHOME_PSTR("new parameter isn't save, can't startSensor"))) {
|
||||
this->set_active(false);
|
||||
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
|
||||
} else if (is(ESPHOME_PSTR("Done"))) {
|
||||
this->set_active(cmd.arg);
|
||||
ESP_LOGI(TAG, "%s", cmd.arg ? LOG_STR_LITERAL("Started sensor") : LOG_STR_LITERAL("Stopped sensor"));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
|
||||
if (!is(PROMPT))
|
||||
return false;
|
||||
ESP_LOGI(TAG, "Restarted sensor.");
|
||||
return true;
|
||||
case CommandType::COMMAND_TYPE_SAVE_CFG:
|
||||
if (is(ESPHOME_PSTR("no parameter has changed"))) {
|
||||
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
|
||||
} else if (is(ESPHOME_PSTR("Done"))) {
|
||||
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Configuration commands, which need the sensor stopped
|
||||
if (is(ESPHOME_PSTR("sensor is not stopped"))) {
|
||||
ESP_LOGE(TAG, "Cannot %s. Sensor is not stopped!",
|
||||
LOG_STR_ARG(BusyActions::get_log_str(static_cast<uint8_t>(cmd.type), 0)));
|
||||
return true;
|
||||
}
|
||||
if (!is(ESPHOME_PSTR("Done")))
|
||||
return false;
|
||||
|
||||
switch (cmd.type) {
|
||||
case CommandType::COMMAND_TYPE_DET_RANGE_CFG:
|
||||
ESP_LOGI(TAG, "Updated detection area config:");
|
||||
for (size_t i = 0; i < 8 && this->det_range_steps_[i] >= 0; i += 2) {
|
||||
ESP_LOGI(TAG, " Detection area %u from %.02fm to %.02fm.", static_cast<unsigned>(i / 2 + 1),
|
||||
this->det_range_steps_[i] * DET_RANGE_STEP_M, this->det_range_steps_[i + 1] * DET_RANGE_STEP_M);
|
||||
}
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SET_LATENCY:
|
||||
ESP_LOGI(TAG,
|
||||
"Updated output latency config:\n"
|
||||
" Signal that someone was detected is delayed by %.03f s.\n"
|
||||
" Signal that nobody is detected anymore is delayed by %.03f s.",
|
||||
this->latency_steps_[0] * LATENCY_STEP_S, this->latency_steps_[1] * LATENCY_STEP_S);
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SENSOR_CFG_START:
|
||||
this->set_start_after_boot(cmd.arg);
|
||||
ESP_LOGI(TAG, "Updated sensor startup behavior: %s",
|
||||
cmd.arg ? LOG_STR_LITERAL("sensor will start automatically after power-on.")
|
||||
: LOG_STR_LITERAL("sensor needs to be started manually after power-on."));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_FACTORY_RESET:
|
||||
ESP_LOGI(TAG, "Sensor factory reset done.");
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_LED_MODE:
|
||||
this->set_led_active(cmd.arg);
|
||||
ESP_LOGI(TAG, "Set led mode done. %s",
|
||||
cmd.arg ? LOG_STR_LITERAL("Sensor LED will blink.") : LOG_STR_LITERAL("Turned off LED."));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_UART_OUTPUT:
|
||||
this->set_uart_presence_active(cmd.arg);
|
||||
ESP_LOGI(TAG, "Set uart mode done. %s",
|
||||
cmd.arg ? LOG_STR_LITERAL("Presence information is sent via UART and GPIO.")
|
||||
: LOG_STR_LITERAL("Presence information is only sent via GPIO."));
|
||||
break;
|
||||
case CommandType::COMMAND_TYPE_SENSITIVITY:
|
||||
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", cmd.arg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *DfrobotSen0395Component::read_message_() {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
|
||||
if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);
|
||||
|
||||
if (byte == ASCII_CR)
|
||||
continue;
|
||||
if (byte >= 0x7F)
|
||||
byte = '?'; // needs to be valid utf8 string for log functions.
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
|
||||
if (this->read_pos_ == 9 && byte == '>')
|
||||
this->read_buffer_[++this->read_pos_] = ASCII_LF;
|
||||
|
||||
if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
|
||||
this->read_buffer_[this->read_pos_] = 0;
|
||||
this->read_pos_ = 0;
|
||||
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
|
||||
return 1; // Full message in buffer
|
||||
} else {
|
||||
this->read_pos_++;
|
||||
const char *line = this->line_reader_.feed(byte);
|
||||
if (line == nullptr && byte == '>' && this->line_reader_.length() == sizeof(PROMPT) - 1) {
|
||||
line = this->line_reader_.partial();
|
||||
this->line_reader_.reset();
|
||||
}
|
||||
}
|
||||
return 0; // No full message yet
|
||||
}
|
||||
|
||||
uint8_t DfrobotSen0395Component::find_prompt_() {
|
||||
if (this->read_message_()) {
|
||||
std::string message(this->read_buffer_);
|
||||
if (message.rfind("leapMMW:/>") != std::string::npos) {
|
||||
return 1; // Prompt found
|
||||
if (line != nullptr) {
|
||||
ESP_LOGV(TAG, "Message: %s", line);
|
||||
return line;
|
||||
}
|
||||
this->line_reader_.log_if_overflowed(TAG);
|
||||
}
|
||||
return 0; // Not found yet
|
||||
}
|
||||
|
||||
uint8_t DfrobotSen0395Component::send_cmd_(const char *cmd, uint32_t duration) {
|
||||
// The interval between two commands must be larger than the specified duration (in ms).
|
||||
if (millis() - ts_last_cmd_sent_ > duration) {
|
||||
this->write_str(cmd);
|
||||
ts_last_cmd_sent_ = millis();
|
||||
return 1; // Command sent
|
||||
}
|
||||
// Could not send command yet as command duration did not fully pass yet.
|
||||
return 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::set_detected_(bool detected) {
|
||||
@@ -99,44 +328,4 @@ void DfrobotSen0395Component::set_detected_(bool detected) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
|
||||
if (this->is_full()) {
|
||||
ESP_LOGE(TAG, "Command queue is full");
|
||||
return -1;
|
||||
} else if (this->is_empty()) {
|
||||
front_++;
|
||||
}
|
||||
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
|
||||
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::unique_ptr<Command> CircularCommandQueue::dequeue() {
|
||||
if (this->is_empty())
|
||||
return nullptr;
|
||||
std::unique_ptr<Command> dequeued_cmd = std::move(commands_[front_]);
|
||||
if (front_ == rear_) {
|
||||
front_ = -1;
|
||||
rear_ = -1;
|
||||
} else {
|
||||
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
|
||||
}
|
||||
|
||||
return dequeued_cmd;
|
||||
}
|
||||
|
||||
bool CircularCommandQueue::is_empty() { return front_ == -1; }
|
||||
|
||||
bool CircularCommandQueue::is_full() { return (rear_ + 1) % COMMAND_QUEUE_SIZE == front_; }
|
||||
|
||||
// Run execute method of first in line command.
|
||||
// Execute is non-blocking and has to be called until it returns 1.
|
||||
uint8_t CircularCommandQueue::process(DfrobotSen0395Component *parent) {
|
||||
if (!is_empty()) {
|
||||
return commands_[front_]->execute(parent);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/line_reader.h"
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
@@ -11,29 +13,30 @@
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
#include "commands.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
|
||||
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 253; // largest LineReader size with a one byte position
|
||||
static constexpr uint8_t COMMAND_QUEUE_SIZE = 20;
|
||||
|
||||
// forward declaration due to circular dependency
|
||||
class DfrobotSen0395Component;
|
||||
// Keep in sync with the BusyActions table in dfrobot_sen0395.cpp
|
||||
enum class CommandType : uint8_t {
|
||||
COMMAND_TYPE_POWER,
|
||||
COMMAND_TYPE_DET_RANGE_CFG,
|
||||
COMMAND_TYPE_SET_LATENCY,
|
||||
COMMAND_TYPE_SENSOR_CFG_START,
|
||||
COMMAND_TYPE_FACTORY_RESET,
|
||||
COMMAND_TYPE_RESET_SYSTEM,
|
||||
COMMAND_TYPE_SAVE_CFG,
|
||||
COMMAND_TYPE_LED_MODE,
|
||||
COMMAND_TYPE_UART_OUTPUT,
|
||||
COMMAND_TYPE_SENSITIVITY,
|
||||
};
|
||||
|
||||
static const uint8_t COMMAND_QUEUE_SIZE = 20;
|
||||
|
||||
class CircularCommandQueue {
|
||||
public:
|
||||
int8_t enqueue(std::unique_ptr<Command> cmd);
|
||||
std::unique_ptr<Command> dequeue();
|
||||
bool is_empty();
|
||||
bool is_full();
|
||||
uint8_t process(DfrobotSen0395Component *parent);
|
||||
|
||||
protected:
|
||||
int front_{-1};
|
||||
int rear_{-1};
|
||||
std::unique_ptr<Command> commands_[COMMAND_QUEUE_SIZE];
|
||||
/// A queued command. arg is the on/off flag, startup mode or sensitivity; the detection range and
|
||||
/// latency values are held by the component.
|
||||
struct Command {
|
||||
CommandType type;
|
||||
uint8_t arg;
|
||||
};
|
||||
|
||||
class DfrobotSen0395Component final : public uart::UARTDevice, public Component {
|
||||
@@ -93,9 +96,21 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
|
||||
}
|
||||
#endif
|
||||
|
||||
int8_t enqueue(std::unique_ptr<Command> cmd);
|
||||
// Commands run one at a time from a queue, at least 1 s apart, so loop() never blocks
|
||||
bool enqueue(CommandType type, uint8_t arg = 0);
|
||||
/// True when count more commands fit; logs when they do not, so a stop/start sequence is never queued in part
|
||||
bool has_queue_room(size_t count) const;
|
||||
/// Queue a detection range config; segments are used up to the first one with a negative bound
|
||||
void enqueue_det_range_cfg(float min1, float max1, float min2, float max2, float min3, float max3, float min4,
|
||||
float max4);
|
||||
/// Queue an output latency config, both delays in seconds
|
||||
void enqueue_set_latency(float delay_after_detection, float delay_after_disappear);
|
||||
/// Queue a setting change, stopping the sensor around it and saving it when the sensor is running
|
||||
void enqueue_saved_setting(CommandType type, bool value);
|
||||
/// Queue a sensor reset
|
||||
void reset_system() { this->enqueue(make_unique<ResetSystemCommand>()); }
|
||||
void reset_system() { this->enqueue(CommandType::COMMAND_TYPE_RESET_SYSTEM); }
|
||||
/// Write the text sent to the sensor for cmd into buf
|
||||
void format_command(const Command &cmd, char *buf, size_t size) const;
|
||||
|
||||
protected:
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -107,19 +122,24 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
|
||||
bool led_active_{false};
|
||||
bool uart_presence_active_{false};
|
||||
bool start_after_boot_{false};
|
||||
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
|
||||
size_t read_pos_{0};
|
||||
CircularCommandQueue cmd_queue_;
|
||||
LineReader<MMWAVE_READ_BUFFER_LENGTH> line_reader_;
|
||||
bool cmd_sent_{false};
|
||||
uint8_t retries_left_{2};
|
||||
StaticRingBuffer<Command, COMMAND_QUEUE_SIZE> cmd_queue_;
|
||||
uint32_t ts_last_cmd_sent_{0};
|
||||
// Range and latency of the queued commands, in sensor steps (0.15 m, 0.025 s); a later command overwrites them
|
||||
int16_t det_range_steps_[8]{};
|
||||
uint16_t latency_steps_[2]{};
|
||||
|
||||
uint8_t read_message_();
|
||||
uint8_t find_prompt_();
|
||||
uint8_t send_cmd_(const char *cmd, uint32_t duration);
|
||||
/// The next complete line, valid until the next call, or nullptr
|
||||
const char *read_message_();
|
||||
void read_state_();
|
||||
/// Run the command at the front of the queue; true when it is done
|
||||
bool run_command_(const Command &cmd, uint32_t now);
|
||||
/// Handle a reply to a sent command; true when the command is done
|
||||
bool handle_reply_(const Command &cmd, const char *message);
|
||||
|
||||
void set_detected_(bool detected);
|
||||
|
||||
friend class Command;
|
||||
friend class ReadStateCommand;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
|
||||
@@ -2,45 +2,18 @@
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique<PowerCommand>(state)); }
|
||||
void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, state); }
|
||||
|
||||
void Sen0395LedSwitch::write_state(bool state) {
|
||||
bool was_active = false;
|
||||
if (this->parent_->is_active()) {
|
||||
was_active = true;
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(false));
|
||||
}
|
||||
this->parent_->enqueue(make_unique<LedModeCommand>(state));
|
||||
this->parent_->enqueue(make_unique<SaveCfgCommand>());
|
||||
if (was_active) {
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(true));
|
||||
}
|
||||
this->parent_->enqueue_saved_setting(CommandType::COMMAND_TYPE_LED_MODE, state);
|
||||
}
|
||||
|
||||
void Sen0395UartPresenceSwitch::write_state(bool state) {
|
||||
bool was_active = false;
|
||||
if (this->parent_->is_active()) {
|
||||
was_active = true;
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(false));
|
||||
}
|
||||
this->parent_->enqueue(make_unique<UartOutputCommand>(state));
|
||||
this->parent_->enqueue(make_unique<SaveCfgCommand>());
|
||||
if (was_active) {
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(true));
|
||||
}
|
||||
this->parent_->enqueue_saved_setting(CommandType::COMMAND_TYPE_UART_OUTPUT, state);
|
||||
}
|
||||
|
||||
void Sen0395StartAfterBootSwitch::write_state(bool state) {
|
||||
bool was_active = false;
|
||||
if (this->parent_->is_active()) {
|
||||
was_active = true;
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(false));
|
||||
}
|
||||
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(state));
|
||||
this->parent_->enqueue(make_unique<SaveCfgCommand>());
|
||||
if (was_active) {
|
||||
this->parent_->enqueue(make_unique<PowerCommand>(true));
|
||||
}
|
||||
this->parent_->enqueue_saved_setting(CommandType::COMMAND_TYPE_SENSOR_CFG_START, state);
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0395
|
||||
|
||||
@@ -5,13 +5,15 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
|
||||
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns
|
||||
|
||||
OutPinLevelSelect = dfrobot_sen0609_ns.class_("OutPinLevelSelect", select.Select)
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
|
||||
cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema(
|
||||
select.ApplySelect,
|
||||
OutPinLevelSelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_SCALE,
|
||||
),
|
||||
@@ -23,8 +25,5 @@ async def to_code(config: ConfigType) -> None:
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
# The level the OUT pin has while a target is present; the index is the module's value
|
||||
await selects(
|
||||
CONF_OUT_PIN_LEVEL,
|
||||
hub.set_out_pin_level_select,
|
||||
options=["low", "high"],
|
||||
apply="control_out_pin_level(index)",
|
||||
CONF_OUT_PIN_LEVEL, hub.set_out_pin_level_select, options=["low", "high"]
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user