mirror of
https://github.com/esphome/esphome.git
synced 2026-10-26 16:02:13 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ca7614820 | ||
|
|
cb9eda3681 | ||
|
|
4e27957c74 | ||
|
|
0856211b9b | ||
|
|
ff4ff5c5f5 | ||
|
|
980b336668 | ||
|
|
a3cb3732ee | ||
|
|
ab50b3830a | ||
|
|
3131cf28a3 |
No files matched your search
@@ -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 TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer).
|
||||
capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver, loopback harness driver with number and binary sensor states and TX frame summarizer), `api/` (raw plaintext native API client for user defined actions), `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 TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer).
|
||||
|
||||
## Templates in `$SKILL/templates`
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
|
||||
- Opening the port usually resets the board through DTR, but not always; `serial_capture.py --reset`
|
||||
pulses RTS to capture from boot. 74880 baud shows the ROM boot messages (`rst cause`, `boot mode`).
|
||||
- UART0 can be looped back onto itself (`USC0(0) |= (1 << UCLBE)` in an `on_boot` lambda) so a UART
|
||||
component is testable without its device: frames written with `uart.write` arrive on RX. Log over the
|
||||
API with `logger: baud_rate: 0`, since the serial port carries the component's bytes.
|
||||
- `esphome logs` on some CH340s emits thousands of duplicated garbage lines right after the RTS reset;
|
||||
a plain pyserial capture at 115200 is reliable.
|
||||
- A failed esptool write can wedge the adapter (`termios` error 22, or an open that hangs) until replug.
|
||||
|
||||
@@ -4,12 +4,13 @@ Each flow names the pull requests it was used on, so their result tables show wh
|
||||
|
||||
One file per component, named after it:
|
||||
|
||||
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266, outgoing connection wake timing on deep sleep and after a Wi-Fi drop
|
||||
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266, outgoing connection wake timing on deep sleep and after a Wi-Fi drop, user defined action listing and execution with a raw plaintext client
|
||||
- [`captive_portal.md`](captive_portal.md): fallback AP and /wifisave hammer
|
||||
- [`cse7766.md`](cse7766.md): Sonoff S31 parser check over the API
|
||||
- [`espnow.md`](espnow.md): ESP8266 broadcast receive
|
||||
- [`hdmi_cec.md`](hdmi_cec.md): two-board wire
|
||||
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback
|
||||
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback, ESP8266 loopback harness without a radar
|
||||
- [`ld2412.md`](ld2412.md): output pin level through the basic configuration command, held back writes
|
||||
- [`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, TLS trust sources (own CA, client certificate, certificate bundle, no verification) with the CLI check
|
||||
- [`nrf52.md`](nrf52.md): dongle board support
|
||||
@@ -20,7 +21,7 @@ One file per component, named after it:
|
||||
- [`scheduler.md`](scheduler.md): loop wake A/B
|
||||
- [`udp.md`](udp.md): datagrams, multicast, sockets, `udp.write` payload shapes
|
||||
- [`usb_cdc_acm.md`](usb_cdc_acm.md): nRF52840 dongle host driver
|
||||
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP)
|
||||
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP), WISP bridge with an ethernet upstream
|
||||
- [`zephyr.md`](zephyr.md): preferences on the nRF52840 dongle
|
||||
- [`zephyr_coredump.md`](zephyr_coredump.md): crash and decode on nRF52 boards
|
||||
- [`zwave_proxy.md`](zwave_proxy.md): frame checksums on a live proxy
|
||||
|
||||
@@ -28,3 +28,15 @@ connected first, and the loop-time maxima. For the loop cost of polling, repeat
|
||||
so after a drop every dial is closed and HA's incoming reconnect wins 11 to 33 s later; the measurable effect is the
|
||||
first dial landing in the loop the network flag flips.
|
||||
Results: https://github.com/esphome/esphome/pull/21085#issuecomment-6254742791
|
||||
|
||||
## User defined action listing and execution without a client library (#18882)
|
||||
|
||||
`scripts/api/rawactions.py HOST list` speaks the plaintext native API over a raw socket and decodes every
|
||||
`ListEntitiesServicesResponse` by field number (argument fields 1 name, 2 type, 3 description, 4 example, 5 optional,
|
||||
6 default_value), so new wire fields show up without waiting for the client library. `rawactions.py HOST exec ACTION
|
||||
k=v ...` sends `ExecuteServiceRequest` with `sint32` ints (field 5, not the legacy int32 field 2), fixed32 floats,
|
||||
length delimited strings and `int[]` as repeated sint32 (field 7); `ALLOW_MISSING=1` leaves arguments out on purpose
|
||||
(the device then drops the call). `templates/api-actions-dut.yaml` (D1 mini, three actions, optional variables with
|
||||
defaults) and `templates/api-actions-strings-dut.yaml` (required strings) log what arrived. String arguments are
|
||||
`StringRef`s that are not null terminated: copy them with `snprintf("%.*s", size, c_str())` into a buffer before
|
||||
logging, and do not pass `%.*s` to `logger.log`, whose validator counts it as one pattern.
|
||||
@@ -0,0 +1,13 @@
|
||||
# ld2412
|
||||
|
||||
## Output pin level and the basic configuration command (#21353)
|
||||
|
||||
`templates/ld2412-select-only-dut.yaml` (ESP32-S3, the module on GPIO43 and GPIO44 at 115200, only the output pin
|
||||
level select, the module's OUT pin on GPIO38 as a binary sensor plus the presence sensor) with `uart: debug:
|
||||
direction: BOTH`. Drive the select with `scripts/ld2450/lddrive.py HOST KEY LOGFILE sel:NAME=OPTION` and read the TX
|
||||
dump for command `02` carrying the level byte, then the OUT pin binary sensor against presence: with level high the
|
||||
pin follows presence, with level low it is inverted. A module that never answers the boot query (no `<<<` lines)
|
||||
only shows the held back path: `Command 02 held back until the module reports its settings`, a query `12`, and after
|
||||
2 s `Command 02 not sent, the module did not report its settings` with the select republished to the last known
|
||||
level. With the full set of numbers configured a select change also needs every number to have a state, so wait
|
||||
for the boot query answer before judging.
|
||||
@@ -13,3 +13,30 @@ selects over the API and prints every LD2450 state update. After each set the hu
|
||||
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
|
||||
|
||||
## ESP8266 loopback harness, no radar needed (#21349)
|
||||
|
||||
`templates/ld2450-loopback.yaml` (package; substitutions `name` and `baud`) runs the hub on UART0 (GPIO1 and GPIO3)
|
||||
with `logger: baud_rate: 0` and sets the UART0 loopback bit in `on_boot` (`USC0(0) |= (1 << UCLBE)`), so everything
|
||||
the hub or a `uart.write` action sends comes back on RX. Template buttons inject periodic frames (all four direction
|
||||
strings, targets 2 and 3), query zone acks (zone type 0, 1 and 2 for the select readback) and call the name based
|
||||
setters with a valid and a bogus name. `uart: debug: direction: TX` delimited on the command footer dumps every
|
||||
command the hub sends. Drive it with `scripts/ld2450/lddrive.py HOST KEY LOGFILE press:NAME sel:NAME=OPTION wait:S`
|
||||
(logs over the API at VERBOSE, 2 s between steps because the API log stream drops lines while `send_command_`
|
||||
blocks), then `txsum.py LOGFILE` lists the TX frames per step; diff the summaries of the two builds. The hub reads its
|
||||
own commands back and logs `Invalid status` for each, which is harmless. The boot readback of the baud rate select
|
||||
comes from the UART setting, so a second build at `baud: "9600"` checks the flash table lookup.
|
||||
Results: https://github.com/esphome/esphome/pull/21349#issuecomment-6332320026
|
||||
|
||||
## Frame splitter collisions (#21357)
|
||||
|
||||
The loopback template also carries the zone 1 numbers and seven collision buttons: a periodic frame with the
|
||||
command footer inside target 1, zone acks with `55 CC` and with `04 03 02 01` inside the coordinates, a periodic
|
||||
frame cut after 12 bytes followed by a zone ack, a frame missing its last byte followed by a complete one, a bogus
|
||||
255 byte length then a frame, and garbage then a frame. Judge by the state updates plus the `Handling Periodic` and
|
||||
`Handling Ack` lines. Through a real radar the set zone ack is sometimes corrupted by the radar itself (its tail
|
||||
replaced by the periodic frame it was sending, for example `04 03 03 03 00 ... 55 CC`), so count readbacks over at
|
||||
least 24 selects per build and read the raw bytes with `uart: debug: direction: RX` (`!extend` the package's uart
|
||||
id) before blaming the parser; the API log stream drops lines under that dump volume, so some misses stay
|
||||
unclassified.
|
||||
Results: https://github.com/esphome/esphome/pull/21357#issuecomment-6333244886
|
||||
@@ -23,3 +23,17 @@ Only an Android phone configures: Settings, network details, Add device, Share W
|
||||
QR scanner makes the phone an enrollee). Render the URI with the `qrcode` module and open the PNG.
|
||||
`esp32: framework: log_level: DEBUG` plus `sdkconfig_options: CONFIG_WPA_DEBUG_PRINT: y` prints the
|
||||
supplicant state machine.
|
||||
|
||||
## WISP bridge with an ethernet upstream (#7147)
|
||||
|
||||
`templates/wisp-ethernet-dut.yaml` (ESP32-S3 with a W5500, `wifi: ap:` with `ap_mode: WISP` and no station) and
|
||||
`templates/wisp-client.yaml` (D1 mini joining the bridge, `http_request` to a web server on the wired LAN and to
|
||||
`http://example.com/` every 15 s, `time: sntp`). Secrets: `wisp_psk` and `lan_http` (serve a text file on the host
|
||||
with `python -m http.server 8787 --bind 0.0.0.0` from a scratch directory). Judge by the client's `GET status 200`
|
||||
lines, the web server log showing the device's ethernet address as the source (NAT), the client's DHCP info (DNS1 is
|
||||
the LAN resolver) and `SNTP synced`. The device logs `Starting AP` and `DHCP DNS server: <resolver>` on its USB
|
||||
console; flashing over the air reboots it for repeat boots. Before the AP address and DHCP server moved ahead of the
|
||||
AP start (the fix in that PR), the first AP start failed on every boot with `esp_netif_dhcps_option failed: 20484`
|
||||
(DHCP server already started by the AP start event) and `Failed to enable NAT`, and the bridge only came up on the
|
||||
retry 90 s later; a client that cannot find the SSID for the first minute on an older build is that. With the fix
|
||||
the bridge is up 6 s after boot. Any change to the ESP-IDF AP start should also rerun the captive portal hammer.
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Plaintext native API client that lists user actions with their raw argument fields and executes one.
|
||||
|
||||
usage: rawactions.py HOST list
|
||||
rawactions.py HOST exec ACTION key=value [...] (every argument must be given; types from the listing)
|
||||
Fields of ListEntitiesServicesArgument: 1 name, 2 type, 3 description, 4 example, 5 optional, 6 default_value.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
HOST = sys.argv[1]
|
||||
CMD = sys.argv[2]
|
||||
ARGS = sys.argv[3:]
|
||||
TYPE_NAMES = {
|
||||
0: "bool",
|
||||
1: "int",
|
||||
2: "float",
|
||||
3: "string",
|
||||
4: "bool[]",
|
||||
5: "int[]",
|
||||
6: "float[]",
|
||||
7: "string[]",
|
||||
}
|
||||
|
||||
|
||||
def varint(v: int) -> bytes:
|
||||
out = bytearray()
|
||||
while True:
|
||||
b = v & 0x7F
|
||||
v >>= 7
|
||||
if v:
|
||||
out.append(b | 0x80)
|
||||
else:
|
||||
out.append(b)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def svarint(v: int) -> bytes:
|
||||
return varint((v << 1) ^ (v >> 63))
|
||||
|
||||
|
||||
def decode_varint(buf: bytes, pos: int) -> tuple[int, int]:
|
||||
value = shift = 0
|
||||
while True:
|
||||
b = buf[pos]
|
||||
pos += 1
|
||||
value |= (b & 0x7F) << shift
|
||||
if not b & 0x80:
|
||||
return value, pos
|
||||
shift += 7
|
||||
|
||||
|
||||
def fields(payload: bytes) -> list[tuple[int, int, object]]:
|
||||
out = []
|
||||
pos = 0
|
||||
while pos < len(payload):
|
||||
key, pos = decode_varint(payload, pos)
|
||||
fnum, wt = key >> 3, key & 7
|
||||
if wt == 0:
|
||||
v, pos = decode_varint(payload, pos)
|
||||
elif wt == 2:
|
||||
ln, pos = decode_varint(payload, pos)
|
||||
v = payload[pos : pos + ln]
|
||||
pos += ln
|
||||
elif wt == 5:
|
||||
v = payload[pos : pos + 4] # fixed32, caller unpacks
|
||||
pos += 4
|
||||
else:
|
||||
raise ValueError(f"wire type {wt}")
|
||||
out.append((fnum, wt, v))
|
||||
return out
|
||||
|
||||
|
||||
def frame(mtype: int, payload: bytes) -> bytes:
|
||||
return b"\x00" + varint(len(payload)) + varint(mtype) + payload
|
||||
|
||||
|
||||
def field_bytes(fnum: int, data: bytes) -> bytes:
|
||||
return varint((fnum << 3) | 2) + varint(len(data)) + data
|
||||
|
||||
|
||||
def field_varint(fnum: int, v: int) -> bytes:
|
||||
return varint((fnum << 3) | 0) + varint(v)
|
||||
|
||||
|
||||
def recv_frames(
|
||||
sock: socket.socket, until: set[int], limit: float = 5.0
|
||||
) -> list[tuple[int, bytes]]:
|
||||
sock.settimeout(limit)
|
||||
buf = bytearray()
|
||||
frames = []
|
||||
while True:
|
||||
try:
|
||||
chunk = sock.recv(4096)
|
||||
except TimeoutError:
|
||||
return frames
|
||||
if not chunk:
|
||||
return frames
|
||||
buf += chunk
|
||||
while True:
|
||||
if len(buf) < 3:
|
||||
break
|
||||
if buf[0] != 0:
|
||||
raise ValueError("not plaintext")
|
||||
try:
|
||||
ln, p = decode_varint(buf, 1)
|
||||
mt, p = decode_varint(buf, p)
|
||||
except IndexError:
|
||||
break
|
||||
if len(buf) < p + ln:
|
||||
break
|
||||
frames.append((mt, bytes(buf[p : p + ln])))
|
||||
del buf[: p + ln]
|
||||
if mt in until:
|
||||
return frames
|
||||
|
||||
|
||||
def main() -> None:
|
||||
sock = socket.create_connection((HOST, 6053), timeout=5)
|
||||
sock.sendall(
|
||||
frame(
|
||||
1, field_bytes(1, b"rawactions") + field_varint(2, 1) + field_varint(3, 10)
|
||||
)
|
||||
) # HelloRequest
|
||||
recv_frames(sock, {2})
|
||||
sock.sendall(frame(3, b"")) # ConnectRequest, no password
|
||||
recv_frames(sock, {4})
|
||||
sock.sendall(frame(11, b"")) # ListEntitiesRequest
|
||||
frames = recv_frames(sock, {19})
|
||||
services = {}
|
||||
for mt, payload in frames:
|
||||
if mt != 41:
|
||||
continue
|
||||
name = key = None
|
||||
args = []
|
||||
for fnum, _, v in fields(payload):
|
||||
if fnum == 1:
|
||||
name = v.decode()
|
||||
elif fnum == 2:
|
||||
key = struct.unpack("<I", v)[0]
|
||||
elif fnum == 3:
|
||||
a = {
|
||||
"name": None,
|
||||
"type": 0,
|
||||
"description": None,
|
||||
"example": None,
|
||||
"optional": None,
|
||||
"default": None,
|
||||
}
|
||||
for af, _, av in fields(v):
|
||||
if af == 1:
|
||||
a["name"] = av.decode()
|
||||
elif af == 2:
|
||||
a["type"] = av
|
||||
elif af == 3:
|
||||
a["description"] = av.decode()
|
||||
elif af == 4:
|
||||
a["example"] = av.decode()
|
||||
elif af == 5:
|
||||
a["optional"] = bool(av)
|
||||
elif af == 6:
|
||||
a["default"] = av.decode()
|
||||
else:
|
||||
a[f"field{af}"] = av
|
||||
args.append(a)
|
||||
services[name] = (key, args)
|
||||
if CMD == "list":
|
||||
for name, (key, args) in services.items():
|
||||
print(f"action {name} key={key}")
|
||||
for a in args:
|
||||
extra = {
|
||||
k: v
|
||||
for k, v in a.items()
|
||||
if k not in ("name", "type") and v is not None
|
||||
}
|
||||
print(f" {a['name']}: {TYPE_NAMES.get(a['type'], a['type'])} {extra}")
|
||||
return
|
||||
name = ARGS[0]
|
||||
key, args = services[name]
|
||||
payload = varint((1 << 3) | 5) + struct.pack("<I", key)
|
||||
given = dict(kv.split("=", 1) for kv in ARGS[1:])
|
||||
for a in args:
|
||||
if a["name"] not in given:
|
||||
if os.environ.get("ALLOW_MISSING"):
|
||||
continue # leave the argument out of the request on purpose
|
||||
sys.exit(f"missing {a['name']}")
|
||||
raw = given[a["name"]]
|
||||
t = a["type"]
|
||||
if t == 0:
|
||||
av = field_varint(1, 1 if raw.lower() == "true" else 0)
|
||||
elif t == 1:
|
||||
av = varint((5 << 3) | 0) + svarint(int(raw)) # sint32 int_
|
||||
elif t == 2:
|
||||
av = varint((3 << 3) | 5) + struct.pack("<f", float(raw))
|
||||
elif t == 3:
|
||||
av = field_bytes(4, raw.encode())
|
||||
elif t == 5:
|
||||
av = b"".join(
|
||||
varint((7 << 3) | 0) + svarint(int(x)) for x in raw.split(",") if x
|
||||
) # repeated sint32 int_array
|
||||
else:
|
||||
sys.exit(f"type {t} not supported here")
|
||||
payload += field_bytes(2, av)
|
||||
sock.sendall(frame(42, payload)) # ExecuteServiceRequest
|
||||
print(f"sent {name} with {given}")
|
||||
sock.close()
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Drive an LD24xx harness over the API: press buttons, set selects and numbers, print state updates, save the log.
|
||||
|
||||
usage: lddrive.py HOST KEY LOGFILE OP [OP ...] (KEY of - means a plaintext API)
|
||||
OP: list | press:NAME | sel:NAME=OPTION | set:NAME=VALUE | wait:SECONDS
|
||||
Steps are 2 s apart: the API log stream drops lines while the hub blocks in send_command_.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
BinarySensorState,
|
||||
ButtonInfo,
|
||||
LogLevel,
|
||||
NumberInfo,
|
||||
NumberState,
|
||||
SelectInfo,
|
||||
SelectState,
|
||||
SensorInfo,
|
||||
SensorState,
|
||||
TextSensorInfo,
|
||||
TextSensorState,
|
||||
)
|
||||
|
||||
HOST, KEY, LOGFILE = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
OPS = sys.argv[4:]
|
||||
T0 = time.time()
|
||||
STEP_GAP_S = 2.0
|
||||
|
||||
|
||||
def stamp() -> str:
|
||||
return f"{time.time() - T0:7.3f}"
|
||||
|
||||
|
||||
async def run(c: APIClient, log) -> None:
|
||||
info = await c.device_info()
|
||||
print(
|
||||
f"device {info.name} esphome {info.esphome_version} compiled {info.compilation_time}"
|
||||
)
|
||||
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)}
|
||||
selects = {e.name: e for e in ents if isinstance(e, SelectInfo)}
|
||||
numbers = {e.name: e for e in ents if isinstance(e, NumberInfo)}
|
||||
|
||||
def on_state(s) -> None:
|
||||
e = by_key.get(s.key)
|
||||
if e is None:
|
||||
return
|
||||
missing = " (missing)" if getattr(s, "missing_state", False) else ""
|
||||
if isinstance(s, TextSensorState):
|
||||
print(f"{stamp()} TXT {e.name} = {s.state!r}{missing}", flush=True)
|
||||
elif isinstance(s, BinarySensorState):
|
||||
print(f"{stamp()} BIN {e.name} = {s.state}{missing}", flush=True)
|
||||
elif isinstance(s, NumberState):
|
||||
print(f"{stamp()} NUM {e.name} = {s.state:.0f}{missing}", flush=True)
|
||||
elif isinstance(s, SelectState):
|
||||
print(f"{stamp()} SEL {e.name} = {s.state!r}{missing}", flush=True)
|
||||
elif (
|
||||
isinstance(s, SensorState)
|
||||
and isinstance(e, SensorInfo)
|
||||
and "Count" in e.name
|
||||
):
|
||||
print(f"{stamp()} CNT {e.name} = {s.state:.0f}", flush=True)
|
||||
|
||||
def on_log(msg) -> None:
|
||||
log.write(f"{stamp()} {msg.message.decode(errors='replace')}\n")
|
||||
|
||||
c.subscribe_states(on_state)
|
||||
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERBOSE)
|
||||
await asyncio.sleep(1.5)
|
||||
for op in OPS:
|
||||
if op == "list":
|
||||
for n in sorted(buttons):
|
||||
print("button:", n)
|
||||
for n in sorted(selects):
|
||||
print("select:", n, selects[n].options)
|
||||
for e in ents:
|
||||
if isinstance(e, TextSensorInfo):
|
||||
print("text_sensor:", e.name)
|
||||
continue
|
||||
kind, _, rest = op.partition(":")
|
||||
if kind == "wait":
|
||||
await asyncio.sleep(float(rest))
|
||||
continue
|
||||
if kind == "press":
|
||||
if rest not in buttons:
|
||||
sys.exit(f"no button named {rest!r}; have {sorted(buttons)}")
|
||||
print(f"{stamp()} >>> press {rest}", flush=True)
|
||||
log.write(f"{stamp()} >>> press {rest}\n")
|
||||
c.button_command(buttons[rest].key)
|
||||
elif kind == "set":
|
||||
name, _, value = rest.partition("=")
|
||||
if name not in numbers:
|
||||
sys.exit(f"no number named {name!r}; have {sorted(numbers)}")
|
||||
print(f"{stamp()} >>> set {name} = {value}", flush=True)
|
||||
log.write(f"{stamp()} >>> set {name} = {value}\n")
|
||||
c.number_command(numbers[name].key, float(value))
|
||||
elif kind == "sel":
|
||||
name, _, value = rest.partition("=")
|
||||
if name not in selects:
|
||||
sys.exit(f"no select named {name!r}; have {sorted(selects)}")
|
||||
print(f"{stamp()} >>> select {name} = {value}", flush=True)
|
||||
log.write(f"{stamp()} >>> select {name} = {value}\n")
|
||||
c.select_command(selects[name].key, value)
|
||||
else:
|
||||
sys.exit(f"unknown op {op!r}")
|
||||
await asyncio.sleep(STEP_GAP_S)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
|
||||
await c.connect(login=True)
|
||||
with Path(LOGFILE).open("w", encoding="utf-8") as log:
|
||||
await run(c, log)
|
||||
await c.disconnect()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Summarize an lddrive.py log: for each step, the LD2450 TX frames, commands and publishes.
|
||||
|
||||
usage: txsum.py LOGFILE (diff the output of two builds)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
||||
out: list[str] = []
|
||||
with Path(sys.argv[1]).open(encoding="utf-8", errors="replace") as log:
|
||||
for raw in log:
|
||||
line = ANSI.sub("", raw.rstrip("\n"))
|
||||
m = re.match(r"\s*[\d.]+ (.*)", line)
|
||||
if not m:
|
||||
continue
|
||||
text = m.group(1)
|
||||
if text.startswith(">>>"):
|
||||
out.append(f"\n# {text[4:]}")
|
||||
elif "[uart_debug" in text:
|
||||
out.append(f" TX {text.split('>>> ')[1]}")
|
||||
elif "[ld2450" in text and (
|
||||
"Sending COMMAND" in text
|
||||
or "zone type" in text.lower()
|
||||
or "Query zone" in text
|
||||
):
|
||||
out.append(f" LOG {text.split(']: ', 1)[1]}")
|
||||
elif "[select:" in text and ">>" in text:
|
||||
out.append(f" SEL {text.split(']: ', 1)[1]}")
|
||||
elif "[text_sensor:098]" in text:
|
||||
out.append(f" TXT {text.split(']: ', 1)[1]}")
|
||||
print("\n".join(out))
|
||||
@@ -0,0 +1,65 @@
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
power_save_mode: none
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
actions:
|
||||
- action: play_buzzer
|
||||
description: Play a song or build one from parts
|
||||
variables:
|
||||
song_str:
|
||||
type: string
|
||||
description: Full RTTTL string
|
||||
example: "scale:d=4,o=5,b=100:c,d,e"
|
||||
required: false
|
||||
notes:
|
||||
type: string
|
||||
required: false
|
||||
octave:
|
||||
type: int
|
||||
default: 5
|
||||
bpm:
|
||||
type: int
|
||||
default: 100
|
||||
tempo:
|
||||
type: float
|
||||
default: 1.5
|
||||
loud:
|
||||
type: bool
|
||||
default: true
|
||||
label:
|
||||
type: string
|
||||
default: ""
|
||||
then:
|
||||
- lambda: |-
|
||||
char s[40], n[40], l[40];
|
||||
snprintf(s, sizeof(s), "%.*s", (int) song_str.size(), song_str.c_str());
|
||||
snprintf(n, sizeof(n), "%.*s", (int) notes.size(), notes.c_str());
|
||||
snprintf(l, sizeof(l), "%.*s", (int) label.size(), label.c_str());
|
||||
ESP_LOGD("main", "play_buzzer song=%s(%d) notes=%s(%d) octave=%d bpm=%d tempo=%.1f loud=%d label=%s(%d)", s,
|
||||
(int) song_str.size(), n, (int) notes.size(), octave, bpm, tempo, loud, l, (int) label.size());
|
||||
- action: plain_action
|
||||
variables:
|
||||
value: int
|
||||
then:
|
||||
- logger.log:
|
||||
format: "plain_action value=%d"
|
||||
args: ["value"]
|
||||
- action: list_action
|
||||
variables:
|
||||
values: int[]
|
||||
then:
|
||||
- logger.log:
|
||||
format: "list_action n=%d"
|
||||
args: ["values.size()"]
|
||||
@@ -0,0 +1,28 @@
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
power_save_mode: none
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
actions:
|
||||
- action: str_action
|
||||
variables:
|
||||
song_str: string
|
||||
notes: string
|
||||
octave: int
|
||||
then:
|
||||
- lambda: |-
|
||||
char s[40], n[40];
|
||||
snprintf(s, sizeof(s), "%.*s", (int) song_str.size(), song_str.c_str());
|
||||
snprintf(n, sizeof(n), "%.*s", (int) notes.size(), notes.c_str());
|
||||
ESP_LOGD("main", "str_action song=%s(%d) notes=%s(%d) octave=%d", s, (int) song_str.size(), n, (int) notes.size(), octave);
|
||||
@@ -0,0 +1,57 @@
|
||||
substitutions:
|
||||
name: ld2412sel
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
board: esp32-s3-devkitc-1
|
||||
flash_size: 8MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
power_save_mode: none
|
||||
|
||||
uart:
|
||||
- id: uart_ld2412
|
||||
tx_pin: GPIO43
|
||||
rx_pin: GPIO44
|
||||
baud_rate: 115200
|
||||
debug:
|
||||
direction: BOTH
|
||||
dummy_receiver: false
|
||||
after:
|
||||
timeout: 10ms
|
||||
bytes: 120
|
||||
|
||||
ld2412:
|
||||
id: ld2412_radar
|
||||
uart_id: uart_ld2412
|
||||
|
||||
select:
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_radar
|
||||
out_pin_level:
|
||||
name: "LD2412 Hardware output pin level"
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: "LD2412 Out Pin"
|
||||
pin: GPIO38
|
||||
- platform: ld2412
|
||||
ld2412_id: ld2412_radar
|
||||
has_target:
|
||||
name: "LD2412 Presence"
|
||||
@@ -0,0 +1,258 @@
|
||||
esphome:
|
||||
name: ${name}
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- lambda: |-
|
||||
// UART0 TX looped back into its own RX so injected frames reach the hub
|
||||
USC0(0) |= (1 << UCLBE);
|
||||
ESP_LOGI("harness", "UART0 loopback on");
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
logger:
|
||||
baud_rate: 0
|
||||
level: VERBOSE
|
||||
|
||||
uart:
|
||||
id: radar_uart
|
||||
tx_pin: GPIO1
|
||||
rx_pin: GPIO3
|
||||
baud_rate: ${baud}
|
||||
debug:
|
||||
direction: TX
|
||||
dummy_receiver: false
|
||||
after:
|
||||
delimiter: [0x04, 0x03, 0x02, 0x01]
|
||||
|
||||
ld2450:
|
||||
id: radar
|
||||
uart_id: radar_uart
|
||||
|
||||
select:
|
||||
- platform: ld2450
|
||||
baud_rate:
|
||||
name: Baud Rate
|
||||
zone_type:
|
||||
name: Zone Type
|
||||
|
||||
text_sensor:
|
||||
- platform: ld2450
|
||||
version:
|
||||
name: Radar Version
|
||||
target_1:
|
||||
direction:
|
||||
name: T1 Direction
|
||||
target_2:
|
||||
direction:
|
||||
name: T2 Direction
|
||||
target_3:
|
||||
direction:
|
||||
name: T3 Direction
|
||||
|
||||
sensor:
|
||||
- platform: ld2450
|
||||
target_count:
|
||||
name: Target Count
|
||||
|
||||
button:
|
||||
# periodic frames: header, 3 x (x, y, speed, resolution), footer
|
||||
- platform: template
|
||||
name: Frame Approach
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC]
|
||||
- platform: template
|
||||
name: Frame Away
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0xE8, 0x83, 0x14, 0x80, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC]
|
||||
- platform: template
|
||||
name: Frame Stationary
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0xE8, 0x83, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC]
|
||||
- platform: template
|
||||
name: Frame None
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC]
|
||||
- platform: template
|
||||
name: Frame T2 T3
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF4, 0x81, 0x20, 0x83, 0x1E, 0x00, 0x64, 0x00,
|
||||
0x2C, 0x01, 0xB0, 0x84, 0x0A, 0x80, 0x64, 0x00,
|
||||
0x55, 0xCC]
|
||||
# query zone acks: header, length, command C1, status 01, 00 00, zone type, 00, 24 coordinate bytes, footer
|
||||
- platform: template
|
||||
name: Zone Ack Disabled
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01]
|
||||
- platform: template
|
||||
name: Zone Ack Detection
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01]
|
||||
- platform: template
|
||||
name: Zone Ack Filter
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x02, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01]
|
||||
# name based setters, the path lambdas use
|
||||
- platform: template
|
||||
name: Set Zone Filter By Name
|
||||
on_press:
|
||||
- lambda: id(radar).set_zone_type("Filter");
|
||||
- platform: template
|
||||
name: Set Zone Bogus By Name
|
||||
on_press:
|
||||
- lambda: id(radar).set_zone_type("Bogus");
|
||||
- platform: template
|
||||
name: Set Baud 57600 By Name
|
||||
on_press:
|
||||
- lambda: id(radar).set_baud_rate("57600");
|
||||
- platform: template
|
||||
name: Set Baud Bogus By Name
|
||||
on_press:
|
||||
- lambda: id(radar).set_baud_rate("Bogus");
|
||||
# target 1 at x 772, y 258: bytes 04 03 02 01 are the command footer
|
||||
- platform: template
|
||||
name: Frame CmdFooter In Payload
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC]
|
||||
# zone ack, type Detection, zone 1 x1 = 0xCC55: bytes 55 CC are the periodic footer
|
||||
- platform: template
|
||||
name: Zone Ack DataFooter In Payload
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x01, 0x00,
|
||||
0x55, 0xCC, 0xC8, 0x00, 0xDC, 0x05, 0xB8, 0x0B, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01]
|
||||
# zone ack, type Filter, zone 1 x1 = 0x0304 and y1 = 0x0102: bytes 04 03 02 01 are the command footer
|
||||
- platform: template
|
||||
name: Zone Ack CmdFooter In Payload
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x02, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01, 0xDC, 0x05, 0xB8, 0x0B, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01]
|
||||
# a periodic frame cut after 12 bytes, then a complete zone ack (type Disabled, zeros)
|
||||
- platform: template
|
||||
name: Cut Frame Then Zone Ack
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
|
||||
0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01]
|
||||
# a periodic frame missing its last byte, then a complete one (target 1 moving away)
|
||||
- platform: template
|
||||
name: Cut Frame Then Frame
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55,
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0xE8, 0x83, 0x14, 0x80, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC]
|
||||
# a command header claiming a 255 byte payload, then a complete periodic frame (target 1 stationary)
|
||||
- platform: template
|
||||
name: Bogus Length Then Frame
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xFD, 0xFC, 0xFB, 0xFA, 0xFF, 0x00,
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0xE8, 0x83, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC]
|
||||
# five garbage bytes, then a complete periodic frame (target 1 approaching)
|
||||
- platform: template
|
||||
name: Garbage Then Frame
|
||||
on_press:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0x01, 0x02, 0x03, 0x42, 0x99,
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC]
|
||||
|
||||
number:
|
||||
- platform: ld2450
|
||||
presence_timeout:
|
||||
name: Presence Timeout
|
||||
zone_1:
|
||||
x1:
|
||||
name: Z1 X1
|
||||
y1:
|
||||
name: Z1 Y1
|
||||
x2:
|
||||
name: Z1 X2
|
||||
y2:
|
||||
name: Z1 Y2
|
||||
@@ -0,0 +1,57 @@
|
||||
substitutions:
|
||||
name: wispcli
|
||||
ap_ssid: wisp-bridge
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: ${ap_ssid}
|
||||
password: !secret wisp_psk
|
||||
power_save_mode: none
|
||||
reboot_timeout: 0s
|
||||
|
||||
http_request:
|
||||
esp8266_disable_ssl_support: true
|
||||
timeout: 8s
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
on_time_sync:
|
||||
- logger.log:
|
||||
format: "SNTP synced"
|
||||
level: INFO
|
||||
|
||||
interval:
|
||||
- interval: 15s
|
||||
then:
|
||||
- http_request.get:
|
||||
url: !secret lan_http
|
||||
capture_response: true
|
||||
on_response:
|
||||
- logger.log:
|
||||
format: "LAN GET status %d body %s"
|
||||
args: ["response->status_code", "body.c_str()"]
|
||||
level: INFO
|
||||
on_error:
|
||||
- logger.log:
|
||||
format: "LAN GET failed"
|
||||
level: WARN
|
||||
- http_request.get:
|
||||
url: http://example.com/
|
||||
on_response:
|
||||
- logger.log:
|
||||
format: "WAN GET status %d"
|
||||
args: ["response->status_code"]
|
||||
level: INFO
|
||||
on_error:
|
||||
- logger.log:
|
||||
format: "WAN GET failed"
|
||||
level: WARN
|
||||
@@ -0,0 +1,40 @@
|
||||
substitutions:
|
||||
name: wispeth
|
||||
ap_ssid: wisp-bridge
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
level: VERBOSE
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: !secret api_key
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: GPIO13
|
||||
mosi_pin: GPIO11
|
||||
miso_pin: GPIO12
|
||||
cs_pin: GPIO14
|
||||
interrupt_pin: GPIO10
|
||||
reset_pin: GPIO9
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
ssid: ${ap_ssid}
|
||||
password: !secret wisp_psk
|
||||
ap_mode: WISP
|
||||
@@ -212,9 +212,10 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
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`, `assign_bits`); pick
|
||||
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
|
||||
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
|
||||
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`, `assign_bits`,
|
||||
`bit_at`); pick the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use
|
||||
`time::decode_bcd_calendar<LAYOUT>` and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`,
|
||||
`DS_BCD_CALENDAR` or `PCF_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.
|
||||
|
||||
+26
-11
@@ -24,6 +24,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, EsphomeError, HexInt, Lambda
|
||||
from esphome.cpp_generator import (
|
||||
CallExpression,
|
||||
Expression,
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
@@ -446,9 +447,17 @@ async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
|
||||
|
||||
|
||||
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
|
||||
# The generated function's parameters; a std::string arg is never copied.
|
||||
# The generated function's parameters: a reference keeps its spelling, so a mutable
|
||||
# one stays mutable; a value is taken by const reference, so a std::string is never copied.
|
||||
return [
|
||||
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
|
||||
(
|
||||
(
|
||||
t
|
||||
if str(cg.safe_exp(t)).rstrip().endswith("&")
|
||||
else cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &")
|
||||
),
|
||||
arg,
|
||||
)
|
||||
for t, arg in args
|
||||
]
|
||||
|
||||
@@ -578,7 +587,7 @@ def apply_action_builder(
|
||||
if isinstance(spec, ApplyBytes):
|
||||
statements.extend(
|
||||
await _render_bytes(
|
||||
spec, config, receiver, parent, lambda_args, table_name
|
||||
spec, config, receiver, parent, args, table_name
|
||||
)
|
||||
)
|
||||
continue
|
||||
@@ -608,10 +617,14 @@ async def _render_bytes(
|
||||
config: ConfigType,
|
||||
receiver: str,
|
||||
parent: str,
|
||||
lambda_args: TemplateArgsType,
|
||||
args: TemplateArgsType,
|
||||
table_name: str,
|
||||
) -> list[str]:
|
||||
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined."""
|
||||
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined.
|
||||
|
||||
The payload lambda keeps the automation's own argument types, as ``TemplatableBytes``
|
||||
gave it, and is always called, so a body that modifies an argument still compiles.
|
||||
"""
|
||||
value = _config_lookup(config, spec.conf_key)
|
||||
if value is None:
|
||||
return []
|
||||
@@ -622,21 +635,23 @@ async def _render_bytes(
|
||||
|
||||
if isinstance(value, Lambda):
|
||||
inner = await cg.process_lambda(
|
||||
value, lambda_args, return_type=cg.std_vector.template(cg.uint8)
|
||||
value, args, return_type=cg.std_vector.template(cg.uint8)
|
||||
)
|
||||
# Never reduced into the outer function, whose own parameters are const
|
||||
call = CallExpression(inner, *[cg.MockObj(name) for _, name in args])
|
||||
return [
|
||||
"{",
|
||||
f" const std::vector<uint8_t> esphome__bytes = ({call_lambda(inner)});",
|
||||
f" const std::vector<uint8_t> esphome__bytes = {call};",
|
||||
f" {send(spec.target, 'esphome__bytes.data(), esphome__bytes.size()')}",
|
||||
"}",
|
||||
]
|
||||
table, size = _static_bytes(table_name, value)
|
||||
# Qualified so a trigger argument cannot shadow a reused table; an empty payload is nullptr
|
||||
args = f"::{table}, {size}" if size else f"{table}, {size}"
|
||||
table_args = f"::{table}, {size}" if size else f"{table}, {size}"
|
||||
if CORE.is_esp8266 and spec.progmem_target is not None:
|
||||
return [send(spec.progmem_target, args)]
|
||||
return [send(spec.progmem_target, table_args)]
|
||||
if not CORE.is_esp8266:
|
||||
return [send(spec.target, args)]
|
||||
return [send(spec.target, table_args)]
|
||||
# A PROGMEM table must be read with aligned loads on ESP8266, so copy it out first;
|
||||
# the length is known here, so the copy is an exact array, on the heap past ``stack``.
|
||||
# An empty payload still gets a one byte array: the raw lwIP socket rejects a null buffer.
|
||||
@@ -649,7 +664,7 @@ async def _render_bytes(
|
||||
)
|
||||
statements = ["{", f" {buffer}"]
|
||||
if size:
|
||||
statements.append(f" esphome::progmem_memcpy({data}, {args});")
|
||||
statements.append(f" esphome::progmem_memcpy({data}, {table_args});")
|
||||
return [*statements, f" {send(spec.target, f'{data}, {size}')}", "}"]
|
||||
|
||||
|
||||
|
||||
@@ -295,11 +295,6 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
|
||||
# Actions
|
||||
SendPacketAction = ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
|
||||
)
|
||||
|
||||
CC1101_ACTION_SCHEMA = cv.Schema(
|
||||
maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)})
|
||||
)
|
||||
@@ -338,23 +333,11 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"cc1101.send_packet",
|
||||
SendPacketAction,
|
||||
SEND_PACKET_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyBytes(CONF_DATA, "transmit_packet({})"),
|
||||
)
|
||||
async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA],
|
||||
args,
|
||||
var.set_data_template,
|
||||
var.set_data_static,
|
||||
"cc1101_data",
|
||||
)
|
||||
return var
|
||||
|
||||
|
||||
_CONFIG_VALIDATORS = {opt.schema: validator for opt, validator in CONFIG_MAP.items()}
|
||||
|
||||
@@ -132,13 +132,4 @@ class CC1101Component final : public Component,
|
||||
bool enter_tx_();
|
||||
};
|
||||
|
||||
// Action Wrappers
|
||||
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->data_.visit([this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::cc1101
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "epaper_waveshare_bwr.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -37,9 +36,17 @@ void EPaperWaveshareBWR::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
const auto bwr = color_to_bwr(color);
|
||||
|
||||
assign_bits(this->buffer_[pos], bit, bwr == BwrState::BWR_BLACK);
|
||||
if (bwr == BwrState::BWR_BLACK) {
|
||||
this->buffer_[pos] |= bit;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bit;
|
||||
}
|
||||
|
||||
assign_bits(this->buffer_[red_offset + pos], bit, bwr == BwrState::BWR_RED);
|
||||
if (bwr == BwrState::BWR_RED) {
|
||||
this->buffer_[red_offset + pos] |= bit;
|
||||
} else {
|
||||
this->buffer_[red_offset + pos] &= ~bit;
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperWaveshareBWR::fill(Color color) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "es8156.h"
|
||||
#include "es8156_const.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -101,7 +100,11 @@ bool ES8156::set_mute_state_(bool mute_state) {
|
||||
|
||||
ESP_LOGV(TAG, "Read ES8156_REG13_DAC_MUTE: %u", reg13);
|
||||
|
||||
assign_bits(reg13, BIT(1) | BIT(2), mute_state);
|
||||
if (mute_state) {
|
||||
reg13 |= BIT(1) | BIT(2);
|
||||
} else {
|
||||
reg13 &= ~(BIT(1) | BIT(2));
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8156_REG13_DAC_MUTE to %u (muted: %s)", reg13, YESNO(mute_state));
|
||||
return this->write_byte(ES8156_REG13_DAC_MUTE, reg13);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "es8311.h"
|
||||
#include "es8311_const.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -155,7 +154,11 @@ bool ES8311::configure_clock_() {
|
||||
// Register 0x06
|
||||
uint8_t reg06;
|
||||
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, ®06));
|
||||
assign_bits(reg06, BIT(5), this->sclk_inverted_);
|
||||
if (this->sclk_inverted_) {
|
||||
reg06 |= BIT(5);
|
||||
} else {
|
||||
reg06 &= ~BIT(5);
|
||||
}
|
||||
reg06 &= 0xE0;
|
||||
if (coefficient->bclk_div < 19) {
|
||||
reg06 |= (coefficient->bclk_div - 1) << 0;
|
||||
@@ -220,7 +223,11 @@ bool ES8311::set_mute_state_(bool mute_state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assign_bits(reg31, BIT(6) | BIT(5), mute_state);
|
||||
if (mute_state) {
|
||||
reg31 |= BIT(6) | BIT(5);
|
||||
} else {
|
||||
reg31 &= ~(BIT(6) | BIT(5));
|
||||
}
|
||||
|
||||
return this->write_byte(ES8311_REG31_DAC, reg31);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::es8388 {
|
||||
@@ -185,7 +184,11 @@ bool ES8388::set_mute_state_(bool mute_state) {
|
||||
// DAC settings that must be preserved. Previously muting overwrote the whole
|
||||
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
|
||||
// could not be unmuted again.
|
||||
assign_bits(value, ES8388_DACCONTROL3_DAC_MUTE, mute_state);
|
||||
if (mute_state) {
|
||||
value |= ES8388_DACCONTROL3_DAC_MUTE;
|
||||
} else {
|
||||
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
|
||||
return this->write_byte(ES8388_DACCONTROL3, value);
|
||||
|
||||
@@ -222,10 +222,13 @@ void ESPHomeOTAComponent::udp_open_(UdpLink &link) {
|
||||
if (!random_bytes(token, sizeof(token)))
|
||||
return;
|
||||
#ifdef ESPHOME_OTA_UDP_LWIP
|
||||
#if USE_NETWORK_IPV4
|
||||
uint32_t ipv4;
|
||||
if (socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&peer), &ipv4)) {
|
||||
ip_addr_set_ip4_u32(&link.peer_ip, ipv4);
|
||||
} else {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
#if USE_NETWORK_IPV6 && LWIP_IPV6
|
||||
inet6_addr_to_ip6addr(ip_2_ip6(&link.peer_ip), &reinterpret_cast<const sockaddr_in6 *>(&peer)->sin6_addr);
|
||||
IP_SET_TYPE_VAL(link.peer_ip, IPADDR_TYPE_V6);
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_RESTART,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_RESTART_ALERT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import factory_reset_ns
|
||||
|
||||
FactoryResetButton = factory_reset_ns.class_(
|
||||
"FactoryResetButton", button.Button, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = button.button_schema(
|
||||
FactoryResetButton,
|
||||
button.ApplyButton,
|
||||
device_class=DEVICE_CLASS_RESTART,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_RESTART_ALERT,
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await button.register_button(var, config)
|
||||
await button.sub_buttons(config).create(
|
||||
config, None, apply="factory_reset::factory_reset_device()"
|
||||
)
|
||||
@@ -13,24 +13,20 @@ namespace esphome::factory_reset {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "factory_reset.button");
|
||||
|
||||
void FactoryResetButton::dump_config() { LOG_BUTTON("", "Factory Reset Button", this); }
|
||||
void FactoryResetButton::press_action() {
|
||||
static void reset_and_reboot() {
|
||||
global_preferences->reset();
|
||||
App.safe_reboot();
|
||||
}
|
||||
|
||||
void factory_reset_device() {
|
||||
ESP_LOGI(TAG, "Resetting");
|
||||
// Let MQTT settle a bit
|
||||
delay(100); // NOLINT
|
||||
#ifdef USE_OPENTHREAD
|
||||
openthread::global_openthread_component->on_factory_reset(FactoryResetButton::factory_reset_callback);
|
||||
openthread::global_openthread_component->on_factory_reset(reset_and_reboot);
|
||||
#else
|
||||
global_preferences->reset();
|
||||
App.safe_reboot();
|
||||
reset_and_reboot();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_OPENTHREAD
|
||||
void FactoryResetButton::factory_reset_callback() {
|
||||
global_preferences->reset();
|
||||
App.safe_reboot();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::factory_reset
|
||||
@@ -1,21 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::factory_reset {
|
||||
|
||||
class FactoryResetButton final : public button::Button, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
#ifdef USE_OPENTHREAD
|
||||
static void factory_reset_callback();
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
/// Run by the factory reset button.
|
||||
void factory_reset_device();
|
||||
|
||||
} // namespace esphome::factory_reset
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "gree.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::gree {
|
||||
|
||||
@@ -25,7 +24,11 @@ void GreeClimate::set_model(Model model) {
|
||||
}
|
||||
|
||||
void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
|
||||
assign_bits(this->mode_bits_, bit_mask, enabled);
|
||||
if (enabled) {
|
||||
this->mode_bits_ |= bit_mask;
|
||||
} else {
|
||||
this->mode_bits_ &= ~bit_mask;
|
||||
}
|
||||
this->transmit_state();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "hitachi_ac344.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::hitachi_ac344 {
|
||||
|
||||
@@ -17,6 +16,15 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con
|
||||
*dst |= ((data & mask) << offset);
|
||||
}
|
||||
|
||||
void set_bit(uint8_t *const data, const uint8_t position, const bool on) {
|
||||
uint8_t mask = 1 << position;
|
||||
if (on) {
|
||||
*data |= mask;
|
||||
} else {
|
||||
*data &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) {
|
||||
for (uint16_t i = 1; i < length; i += 2) {
|
||||
// Code done this way to avoid a compiler warning bug.
|
||||
@@ -116,7 +124,7 @@ bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC3
|
||||
|
||||
void HitachiClimate::set_swing_v_(bool on) {
|
||||
set_swing_v_toggle_(on); // Set the button value.
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_SWINGV_BYTE], 1 << HITACHI_AC344_SWINGV_OFFSET, on);
|
||||
set_bit(&remote_state_[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET, on);
|
||||
}
|
||||
|
||||
bool HitachiClimate::get_swing_v_() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "hitachi_ac424.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::hitachi_ac424 {
|
||||
|
||||
@@ -17,6 +16,15 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con
|
||||
*dst |= ((data & mask) << offset);
|
||||
}
|
||||
|
||||
void set_bit(uint8_t *const data, const uint8_t position, const bool on) {
|
||||
uint8_t mask = 1 << position;
|
||||
if (on) {
|
||||
*data |= mask;
|
||||
} else {
|
||||
*data &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) {
|
||||
for (uint16_t i = 1; i < length; i += 2) {
|
||||
// Code done this way to avoid a compiler warning bug.
|
||||
@@ -116,7 +124,7 @@ bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC4
|
||||
|
||||
void HitachiClimate::set_swing_v_(bool on) {
|
||||
set_swing_v_toggle_(on); // Set the button value.
|
||||
assign_bits(this->remote_state_[HITACHI_AC424_SWINGV_BYTE], 1 << HITACHI_AC424_SWINGV_OFFSET, on);
|
||||
set_bit(&remote_state_[HITACHI_AC424_SWINGV_BYTE], HITACHI_AC424_SWINGV_OFFSET, on);
|
||||
}
|
||||
|
||||
bool HitachiClimate::get_swing_v_() {
|
||||
|
||||
@@ -33,22 +33,22 @@ uint16_t parse_u8_hb_60(OpenthermData &data) { return data.valueHB * 60; }
|
||||
int16_t parse_s16(OpenthermData &data) { return data.get_s16(); }
|
||||
float parse_f88(OpenthermData &data) { return data.get_f88(); }
|
||||
|
||||
void write_flag8_lb_0(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 0, value); }
|
||||
void write_flag8_lb_1(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 1, value); }
|
||||
void write_flag8_lb_2(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 2, value); }
|
||||
void write_flag8_lb_3(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 3, value); }
|
||||
void write_flag8_lb_4(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 4, value); }
|
||||
void write_flag8_lb_5(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 5, value); }
|
||||
void write_flag8_lb_6(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 6, value); }
|
||||
void write_flag8_lb_7(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 7, value); }
|
||||
void write_flag8_hb_0(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 0, value); }
|
||||
void write_flag8_hb_1(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 1, value); }
|
||||
void write_flag8_hb_2(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 2, value); }
|
||||
void write_flag8_hb_3(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 3, value); }
|
||||
void write_flag8_hb_4(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 4, value); }
|
||||
void write_flag8_hb_5(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 5, value); }
|
||||
void write_flag8_hb_6(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 6, value); }
|
||||
void write_flag8_hb_7(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 7, value); }
|
||||
void write_flag8_lb_0(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 0, value); }
|
||||
void write_flag8_lb_1(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 1, value); }
|
||||
void write_flag8_lb_2(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 2, value); }
|
||||
void write_flag8_lb_3(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 3, value); }
|
||||
void write_flag8_lb_4(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 4, value); }
|
||||
void write_flag8_lb_5(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 5, value); }
|
||||
void write_flag8_lb_6(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 6, value); }
|
||||
void write_flag8_lb_7(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 7, value); }
|
||||
void write_flag8_hb_0(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 0, value); }
|
||||
void write_flag8_hb_1(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 1, value); }
|
||||
void write_flag8_hb_2(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 2, value); }
|
||||
void write_flag8_hb_3(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 3, value); }
|
||||
void write_flag8_hb_4(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 4, value); }
|
||||
void write_flag8_hb_5(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 5, value); }
|
||||
void write_flag8_hb_6(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 6, value); }
|
||||
void write_flag8_hb_7(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 7, value); }
|
||||
void write_u8_lb(const uint8_t value, OpenthermData &data) { data.valueLB = value; }
|
||||
void write_u8_hb(const uint8_t value, OpenthermData &data) { data.valueHB = value; }
|
||||
void write_s8_lb(const int8_t value, OpenthermData &data) { data.valueLB = (uint8_t) value; }
|
||||
|
||||
@@ -20,6 +20,14 @@ namespace esphome::opentherm {
|
||||
|
||||
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
|
||||
|
||||
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
|
||||
|
||||
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
|
||||
|
||||
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
|
||||
return bit_value ? set_bit(value, bit) : clear_bit(value, bit);
|
||||
}
|
||||
|
||||
enum OperationMode {
|
||||
IDLE = 0, // no operation
|
||||
|
||||
|
||||
@@ -90,7 +90,11 @@ void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y % 8;
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
}
|
||||
|
||||
void PCD8544::dump_config() {
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_RESTART,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_RESTART,
|
||||
)
|
||||
from esphome.const import DEVICE_CLASS_RESTART, ENTITY_CATEGORY_CONFIG, ICON_RESTART
|
||||
from esphome.types import ConfigType
|
||||
|
||||
restart_ns = cg.esphome_ns.namespace("restart")
|
||||
RestartButton = restart_ns.class_("RestartButton", button.Button, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = button.button_schema(
|
||||
RestartButton,
|
||||
button.ApplyButton,
|
||||
icon=ICON_RESTART,
|
||||
device_class=DEVICE_CLASS_RESTART,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await button.register_button(var, config)
|
||||
await button.sub_buttons(config).create(
|
||||
config, None, apply="restart::restart_device()"
|
||||
)
|
||||
@@ -7,12 +7,11 @@ namespace esphome::restart {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "restart.button");
|
||||
|
||||
void RestartButton::press_action() {
|
||||
void restart_device() {
|
||||
ESP_LOGI(TAG, "Restarting device");
|
||||
// Let MQTT settle a bit
|
||||
delay(100); // NOLINT
|
||||
App.safe_reboot();
|
||||
}
|
||||
void RestartButton::dump_config() { LOG_BUTTON("", "Restart Button", this); }
|
||||
|
||||
} // namespace esphome::restart
|
||||
@@ -1,19 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::restart {
|
||||
|
||||
class RestartButton final : public button::Button, public Component {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) RestartButton()` would zero-fill .bss that is already zero.
|
||||
RestartButton() {}
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
/// Run by the restart button.
|
||||
void restart_device();
|
||||
|
||||
} // namespace esphome::restart
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "runtime_image.h"
|
||||
#include "image_decoder.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -92,7 +91,11 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
auto on = is_color_on(color);
|
||||
if (this->has_transparency() && color.w < 0x80)
|
||||
on = false;
|
||||
assign_bits(this->buffer_[pos], bitno, on);
|
||||
if (on) {
|
||||
this->buffer_[pos] |= bitno;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bitno;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case image::IMAGE_TYPE_RGB111: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -9,26 +8,18 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import SafeModeComponent, safe_mode_ns
|
||||
from .. import SafeModeComponent
|
||||
|
||||
DEPENDENCIES = ["safe_mode"]
|
||||
|
||||
SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
button.button_schema(
|
||||
SafeModeButton,
|
||||
device_class=DEVICE_CLASS_RESTART,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_RESTART_ALERT,
|
||||
)
|
||||
.extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
CONFIG_SCHEMA = button.button_schema(
|
||||
button.ApplyButton,
|
||||
device_class=DEVICE_CLASS_RESTART,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_RESTART_ALERT,
|
||||
).extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await button.new_button(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await cg.add_variable(var.set_safe_mode, config[CONF_SAFE_MODE])
|
||||
buttons = button.sub_buttons(config, parent=config[CONF_SAFE_MODE])
|
||||
await buttons.create(config, None, apply="restart_in_safe_mode()")
|
||||
@@ -1,21 +0,0 @@
|
||||
#include "safe_mode_button.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::safe_mode {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "safe_mode.button");
|
||||
|
||||
void SafeModeButton::press_action() {
|
||||
ESP_LOGI(TAG, "Restarting in safe mode");
|
||||
this->safe_mode_component_->set_safe_mode_pending(true);
|
||||
|
||||
// Let MQTT settle a bit
|
||||
delay(100); // NOLINT
|
||||
App.safe_reboot();
|
||||
}
|
||||
|
||||
void SafeModeButton::dump_config() { LOG_BUTTON("", "Safe Mode Button", this); }
|
||||
|
||||
} // namespace esphome::safe_mode
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "esphome/components/safe_mode/safe_mode.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::safe_mode {
|
||||
|
||||
class SafeModeButton final : public button::Button, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void set_safe_mode(SafeModeComponent *safe_mode_component) { this->safe_mode_component_ = safe_mode_component; }
|
||||
|
||||
protected:
|
||||
SafeModeComponent *safe_mode_component_;
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::safe_mode
|
||||
@@ -174,6 +174,14 @@ bool SafeModeComponent::get_safe_mode_pending() {
|
||||
return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
|
||||
}
|
||||
|
||||
void SafeModeComponent::restart_in_safe_mode() {
|
||||
ESP_LOGI(TAG, "Restarting in safe mode");
|
||||
this->set_safe_mode_pending(true);
|
||||
// Let MQTT settle a bit
|
||||
delay(100); // NOLINT
|
||||
App.safe_reboot();
|
||||
}
|
||||
|
||||
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after,
|
||||
bool in_flash) {
|
||||
this->safe_mode_start_time_ = millis();
|
||||
|
||||
@@ -25,6 +25,8 @@ class SafeModeComponent final : public Component {
|
||||
/// Set to true if the next startup will enter safe mode
|
||||
void set_safe_mode_pending(const bool &pending);
|
||||
bool get_safe_mode_pending();
|
||||
/// Set safe mode pending and reboot; run by the safe mode button.
|
||||
void restart_in_safe_mode();
|
||||
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_POWER
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_POWER
|
||||
from esphome.types import ConfigType
|
||||
|
||||
shutdown_ns = cg.esphome_ns.namespace("shutdown")
|
||||
ShutdownButton = shutdown_ns.class_("ShutdownButton", button.Button, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = button.button_schema(
|
||||
ShutdownButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await button.register_button(var, config)
|
||||
await button.sub_buttons(config).create(
|
||||
config, None, apply="shutdown::shutdown_device()"
|
||||
)
|
||||
@@ -14,8 +14,7 @@ namespace esphome::shutdown {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "shutdown.button");
|
||||
|
||||
void ShutdownButton::dump_config() { LOG_BUTTON("", "Shutdown Button", this); }
|
||||
void ShutdownButton::press_action() {
|
||||
void shutdown_device() {
|
||||
ESP_LOGI(TAG, "Shutting down");
|
||||
// Let MQTT settle a bit
|
||||
delay(100); // NOLINT
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/button/button.h"
|
||||
|
||||
namespace esphome::shutdown {
|
||||
|
||||
class ShutdownButton final : public button::Button, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
/// Run by the shutdown button.
|
||||
void shutdown_device();
|
||||
|
||||
} // namespace esphome::shutdown
|
||||
@@ -17,6 +17,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
idf_version,
|
||||
only_on_variant,
|
||||
)
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
@@ -142,13 +143,19 @@ TYPE_CLASS = {
|
||||
}
|
||||
|
||||
|
||||
def _require_psram_dma_idf(value: Any) -> Any:
|
||||
if idf_version() < cv.Version(5, 5, 3):
|
||||
raise cv.Invalid("psram_dma requires ESP-IDF 5.5.3 or newer")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_psram_dma(value: Any) -> bool:
|
||||
value = cv.boolean(value)
|
||||
if not value:
|
||||
return value
|
||||
return cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.only_with_framework("esp-idf"),
|
||||
_require_psram_dma_idf,
|
||||
only_on_variant(
|
||||
supported=[
|
||||
VARIANT_ESP32C5,
|
||||
@@ -159,7 +166,6 @@ def _validate_psram_dma(value: Any) -> bool:
|
||||
],
|
||||
msg_prefix="PSRAM DMA",
|
||||
),
|
||||
cv.require_framework_version(esp_idf=cv.Version(5, 5, 3)),
|
||||
cv.requires_component(PSRAM_DOMAIN),
|
||||
)(value)
|
||||
|
||||
|
||||
@@ -342,7 +342,11 @@ void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y & 0x07;
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
}
|
||||
void SSD1306::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::st7305 {
|
||||
@@ -192,7 +191,11 @@ void HOT ST7305::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
// The init sequence enables INVON, so a set bit is white and a clear bit is black.
|
||||
uint8_t &value = this->buffer_[index];
|
||||
const uint8_t previous = value;
|
||||
assign_bits(value, mask, color.is_on());
|
||||
if (color.is_on()) {
|
||||
value |= mask;
|
||||
} else {
|
||||
value &= ~mask;
|
||||
}
|
||||
if (value == previous)
|
||||
return;
|
||||
this->extend_dirty_(x, y, x, y);
|
||||
|
||||
@@ -123,7 +123,11 @@ void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y & 0x07;
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
}
|
||||
|
||||
void ST7567::fill(Color color) {
|
||||
|
||||
@@ -11,10 +11,9 @@ from esphome.components.const import (
|
||||
CONF_RST_PIN,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID, TimePeriod
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DOMAIN = "sx126x"
|
||||
MULTI_CONF = True
|
||||
@@ -133,10 +132,6 @@ SHAPING = {
|
||||
"NONE": SX126xPulseShape.NO_FILTER,
|
||||
}
|
||||
|
||||
SendPacketAction = sx126x_ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(SX126x)
|
||||
)
|
||||
|
||||
|
||||
def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
if isinstance(value, str):
|
||||
@@ -324,25 +319,8 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"sx126x.send_packet",
|
||||
SendPacketAction,
|
||||
SEND_PACKET_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyBytes(CONF_DATA, "transmit_packet({})", stack=MAX_PACKET_SIZE),
|
||||
)
|
||||
async def send_packet_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA],
|
||||
args,
|
||||
var.set_data_template,
|
||||
var.set_data_static,
|
||||
"sx126x_data",
|
||||
)
|
||||
return var
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sx126x/sx126x.h"
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<SX126x> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
this->data_.template visit<SX126X_MAX_PACKET_SIZE>(
|
||||
[this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -11,9 +11,7 @@ from esphome.components.const import (
|
||||
CONF_RST_PIN,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.const import CONF_DATA, CONF_FREQUENCY
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DOMAIN = "sx127x"
|
||||
@@ -117,10 +115,6 @@ SHAPING = {
|
||||
"NONE": SX127xPaRamp.SHAPING_NONE,
|
||||
}
|
||||
|
||||
SendPacketAction = sx127x_ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(SX127x)
|
||||
)
|
||||
|
||||
|
||||
def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
if isinstance(value, str):
|
||||
@@ -291,25 +285,8 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"sx127x.send_packet",
|
||||
SendPacketAction,
|
||||
SEND_PACKET_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyBytes(CONF_DATA, "transmit_packet({})", stack=MAX_PACKET_SIZE),
|
||||
)
|
||||
async def send_packet_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA],
|
||||
args,
|
||||
var.set_data_template,
|
||||
var.set_data_static,
|
||||
"sx127x_data",
|
||||
)
|
||||
return var
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sx127x/sx127x.h"
|
||||
|
||||
namespace esphome::sx127x {
|
||||
|
||||
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<SX127x> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
public:
|
||||
void play(const Ts &...x) override {
|
||||
this->data_.template visit<SX127X_MAX_PACKET_SIZE>(
|
||||
[this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::sx127x
|
||||
@@ -121,7 +121,6 @@ HOST_BAUD_RATES = [
|
||||
]
|
||||
|
||||
UARTDevice = uart_ns.class_("UARTDevice")
|
||||
UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action)
|
||||
UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action)
|
||||
UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component)
|
||||
MULTI_CONF = True
|
||||
@@ -593,9 +592,8 @@ def new_uart_component(
|
||||
return cg.new_bus_component(register_uart_device, config, *args)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"uart.write",
|
||||
UARTWriteAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(UARTComponent),
|
||||
@@ -603,15 +601,11 @@ def new_uart_component(
|
||||
},
|
||||
key=CONF_DATA,
|
||||
),
|
||||
synchronous=True,
|
||||
# A constant stays in flash: write_array_progmem copies it out in chunks on ESP8266
|
||||
automation.ApplyBytes(
|
||||
CONF_DATA, "write_array({})", progmem_target="write_array_progmem({})"
|
||||
),
|
||||
)
|
||||
async def uart_write_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA], args, var.set_data_template, var.set_data_static, "uart_data"
|
||||
)
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
template<typename... Ts> class UARTWriteAction final : public Action<Ts...>, public Parented<UARTComponent> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->data_.is_static()) {
|
||||
this->parent_->write_array_progmem(this->data_.data(), this->data_.size());
|
||||
} else {
|
||||
this->parent_->write_array(this->data_.value(x...));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::uart
|
||||
@@ -29,7 +29,6 @@ DOMAIN = "udp"
|
||||
MULTI_CONF = True
|
||||
udp_ns = cg.esphome_ns.namespace("udp")
|
||||
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
||||
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
||||
trigger_argname = "data"
|
||||
# Listener callback type (non-owning span from UDP component)
|
||||
listener_args = cg.std_span.template(cg.uint8.operator("const"))
|
||||
@@ -240,17 +239,23 @@ def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
_UDP_WRITE_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(UDPComponent),
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
key=CONF_DATA,
|
||||
)
|
||||
_udp_write_builder = automation.apply_action_builder(
|
||||
"udp.write",
|
||||
UDPWriteAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(UDPComponent),
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
key=CONF_DATA,
|
||||
),
|
||||
synchronous=True,
|
||||
_UDP_WRITE_SCHEMA,
|
||||
# Constant payloads up to this size are sent from the stack on ESP8266
|
||||
automation.ApplyBytes(CONF_DATA, "send_packet({})", stack=128),
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"udp.write", automation.ApplyAction, _UDP_WRITE_SCHEMA, synchronous=True
|
||||
)
|
||||
async def udp_write_to_code(
|
||||
config: ConfigType,
|
||||
@@ -258,11 +263,6 @@ async def udp_write_to_code(
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
udp_var = await cg.get_variable(config[CONF_ID])
|
||||
await cg.register_parented(var, udp_var)
|
||||
cg.add(udp_var.set_should_broadcast())
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA], args, var.set_data_template, var.set_data_static, "udp_data"
|
||||
)
|
||||
return var
|
||||
return await _udp_write_builder(config, action_id, template_arg, args)
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "udp_component.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome::udp {
|
||||
|
||||
/// Constant payloads up to this size are sent from the stack on ESP8266.
|
||||
static constexpr size_t STACK_PAYLOAD_SIZE = 128;
|
||||
|
||||
template<typename... Ts> class UDPWriteAction final : public Action<Ts...>, public Parented<UDPComponent> {
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// One sendto per packet, so a failed allocation never sends a partial packet
|
||||
this->data_.template visit<STACK_PAYLOAD_SIZE>(
|
||||
[this](const uint8_t *data, size_t len) { this->parent_->send_packet(data, len); }, x...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::udp
|
||||
|
||||
#endif
|
||||
@@ -1,12 +1,6 @@
|
||||
from esphome import automation, core
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import (
|
||||
media_player,
|
||||
micro_wake_word,
|
||||
microphone,
|
||||
network,
|
||||
speaker,
|
||||
)
|
||||
from esphome.components import media_player, micro_wake_word, microphone, speaker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
@@ -175,7 +169,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.key_requires((CONF_ON_TTS_STREAM_START, CONF_ON_TTS_STREAM_END), CONF_SPEAKER),
|
||||
network.require_ipv4("voice_assistant"),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
@@ -82,7 +82,11 @@ void VoiceAssistant::setup() {
|
||||
float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
bool VoiceAssistant::start_udp_socket_() {
|
||||
#if USE_NETWORK_IPV4
|
||||
this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
#else
|
||||
this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
|
||||
#endif
|
||||
if (this->socket_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not create socket");
|
||||
this->mark_failed();
|
||||
@@ -663,17 +667,20 @@ void VoiceAssistant::start_streaming(struct sockaddr_storage *addr, uint16_t por
|
||||
}
|
||||
|
||||
memcpy(&this->dest_addr_, addr, sizeof(this->dest_addr_));
|
||||
if (this->dest_addr_.ss_family == AF_INET) {
|
||||
((struct sockaddr_in *) &this->dest_addr_)->sin_port = htons(port);
|
||||
}
|
||||
#if LWIP_IPV6
|
||||
else if (this->dest_addr_.ss_family == AF_INET6) {
|
||||
((struct sockaddr_in6 *) &this->dest_addr_)->sin6_port = htons(port);
|
||||
}
|
||||
switch (this->dest_addr_.ss_family) {
|
||||
#if USE_NETWORK_IPV4
|
||||
case AF_INET:
|
||||
((struct sockaddr_in *) &this->dest_addr_)->sin_port = htons(port);
|
||||
break;
|
||||
#endif
|
||||
else {
|
||||
ESP_LOGW(TAG, "Unknown address family: %d", this->dest_addr_.ss_family);
|
||||
return;
|
||||
#if LWIP_IPV6
|
||||
case AF_INET6:
|
||||
((struct sockaddr_in6 *) &this->dest_addr_)->sin6_port = htons(port);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown address family: %d", this->dest_addr_.ss_family);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only primary microphone channel over UDP
|
||||
|
||||
@@ -224,8 +224,8 @@ WIFI_AP_MODE_WISP = "WISP"
|
||||
def _validate_ap_mode(value: str) -> str:
|
||||
value = cv.one_of(WIFI_AP_MODE_FALLBACK, WIFI_AP_MODE_WISP, upper=True)(value)
|
||||
if value == WIFI_AP_MODE_WISP:
|
||||
# WISP mode routes through NAT, which only ESP-IDF provides
|
||||
cv.only_with_framework(Framework.ESP_IDF)(value)
|
||||
# The bridge is part of the ESP32 backend, on both of its frameworks
|
||||
cv.only_on_esp32(value)
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -533,7 +533,8 @@ class SubEntities:
|
||||
"""Create the entity from ``conf``; ``setter`` may be None when the hub keeps no pointer.
|
||||
|
||||
With ``apply`` the entity is the domain's ``Apply*`` class and its control runs
|
||||
``parent->apply``, with ``publish`` ("before"/"after") placing the domain's publish.
|
||||
``parent->apply``, or ``apply`` as a namespaced free statement when no parent is bound, with
|
||||
``publish`` ("before"/"after") placing the domain's publish.
|
||||
"""
|
||||
entity_id: ID = conf[CONF_ID]
|
||||
is_apply_class = self.apply_entity is not None and str(entity_id.type) == str(
|
||||
@@ -545,8 +546,13 @@ class SubEntities:
|
||||
raise ValueError(
|
||||
f"{entity_id}: apply= needs the domain's Apply class, not {entity_id.type}"
|
||||
)
|
||||
if apply is not None and self.parent is None:
|
||||
raise ValueError(f"{entity_id}: apply= needs a parent")
|
||||
# A parentless statement is a namespaced free function, so a hub platform that forgot
|
||||
# parent= fails here instead of in the generated C++
|
||||
if apply is not None and self.parent is None and "::" not in apply:
|
||||
raise ValueError(
|
||||
f"{entity_id}: apply= without a parent must call a namespaced free function "
|
||||
f"(ns::fn()); bind parent= for a hub method"
|
||||
)
|
||||
if publish is not None and (apply is None or self.apply_entity.publish is None):
|
||||
raise ValueError(f"{entity_id}: publish= needs apply= on a stateful domain")
|
||||
if publish not in (None, "before", "after"):
|
||||
@@ -571,7 +577,7 @@ class SubEntities:
|
||||
if isinstance(parent, ID):
|
||||
parent = await get_variable(parent)
|
||||
apply_entity = self.apply_entity
|
||||
body = [f"{parent_ref(parent)}->{apply};"]
|
||||
body = [f"{apply};" if parent is None else f"{parent_ref(parent)}->{apply};"]
|
||||
if publish == "before":
|
||||
body.insert(0, apply_entity.publish)
|
||||
elif publish == "after":
|
||||
|
||||
@@ -1077,6 +1077,9 @@ template<typename T> ESPHOME_ALWAYS_INLINE constexpr void assign_bits(T &value,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether bit \p bit (0 is the LSB) of \p value is set; the read side of assign_bits().
|
||||
template<typename T> ESPHOME_ALWAYS_INLINE constexpr bool bit_at(T value, unsigned bit) { return (value >> bit) & 1; }
|
||||
|
||||
/// Reverse the order of 8 bits.
|
||||
inline uint8_t reverse_bits(uint8_t x) {
|
||||
x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_constant_payloads_share_progmem_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Equal send_packet payloads share one table; lambdas stay templates."""
|
||||
"""Equal send_packet payloads share one table; a lambda is sent from its vector."""
|
||||
main_cpp = generate_main(component_config_path("payload_tables.yaml"))
|
||||
|
||||
tables = re.findall(
|
||||
@@ -23,8 +23,10 @@ def test_constant_payloads_share_progmem_tables(
|
||||
assert [v for _, v in tables] == [
|
||||
"{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}"
|
||||
]
|
||||
assert main_cpp.count(f"set_data_static({tables[0][0]}, 8);") == 2
|
||||
assert "set_data_template(" in main_cpp
|
||||
assert main_cpp.count(f"->transmit_packet(::{tables[0][0]}, 8);") == 2
|
||||
assert (
|
||||
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [64, 65])
|
||||
|
||||
@@ -5,6 +5,7 @@ import pytest
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S3,
|
||||
@@ -35,11 +36,17 @@ def _stage(
|
||||
platform_framework: PlatformFramework,
|
||||
variant: str,
|
||||
version: cv.Version,
|
||||
idf: cv.Version | None = None,
|
||||
) -> None:
|
||||
"""``idf`` is the ESP-IDF underneath an Arduino ``version``; an ESP-IDF build is its own."""
|
||||
set_core_config(
|
||||
platform_framework,
|
||||
core_data={KEY_FRAMEWORK_VERSION: version},
|
||||
platform_data={KEY_BOARD: "test-board", KEY_VARIANT: variant},
|
||||
platform_data={
|
||||
KEY_BOARD: "test-board",
|
||||
KEY_VARIANT: variant,
|
||||
KEY_IDF_VERSION: idf or version,
|
||||
},
|
||||
)
|
||||
CORE.loaded_integrations.add("psram")
|
||||
|
||||
@@ -74,17 +81,46 @@ def test_psram_dma_accepts_esp32s31(
|
||||
assert config[CONF_PSRAM_DMA] is True
|
||||
|
||||
|
||||
def test_psram_dma_rejects_arduino(
|
||||
def test_psram_dma_accepts_arduino_on_idf_5_5_3(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""The ESP-IDF underneath an Arduino build is what counts; 3.3.7 is the first on 5.5.3."""
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
VARIANT_ESP32S3,
|
||||
cv.Version(3, 3, 7),
|
||||
idf=cv.Version(5, 5, 3),
|
||||
)
|
||||
|
||||
config = _schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
assert config[CONF_PSRAM_DMA] is True
|
||||
|
||||
|
||||
def test_psram_dma_rejects_other_platforms(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""No ESP32 state to read a version from: the platform check comes first."""
|
||||
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
||||
CORE.loaded_integrations.add("psram")
|
||||
|
||||
with pytest.raises(cv.Invalid, match="only available on"):
|
||||
_schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
|
||||
def test_psram_dma_rejects_older_arduino(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_stage(
|
||||
set_core_config,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
VARIANT_ESP32S3,
|
||||
cv.Version(5, 5, 3),
|
||||
cv.Version(3, 3, 6),
|
||||
idf=cv.Version(5, 5, 2),
|
||||
)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="only available with framework"):
|
||||
with pytest.raises(cv.Invalid, match="requires ESP-IDF 5.5.3"):
|
||||
_schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
|
||||
@@ -127,7 +163,7 @@ def test_psram_dma_rejects_older_idf(
|
||||
cv.Version(5, 5, 2),
|
||||
)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="requires at least framework version 5.5.3"):
|
||||
with pytest.raises(cv.Invalid, match="requires ESP-IDF 5.5.3"):
|
||||
_schema()({CONF_PSRAM_DMA: True})
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_constant_packets_share_progmem_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Equal constant packets share one table; lambdas stay templates."""
|
||||
"""Equal constant packets share one table; a lambda is sent from its vector."""
|
||||
main_cpp = generate_main(component_config_path("packet_tables.yaml"))
|
||||
|
||||
tables = dict(
|
||||
@@ -26,8 +26,10 @@ def test_constant_packets_share_progmem_tables(
|
||||
["{0xC5, 0x51, 0x78, 0x82}", "{0x68, 0x69}"]
|
||||
)
|
||||
shared = next(k for k, v in tables.items() if v == "{0xC5, 0x51, 0x78, 0x82}")
|
||||
assert main_cpp.count(f"set_data_static({shared}, 4);") == 2
|
||||
assert "set_data_template(" in main_cpp
|
||||
assert main_cpp.count(f"->transmit_packet(::{shared}, 4);") == 2
|
||||
assert (
|
||||
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
)
|
||||
assert "set_sx1261" not in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_constant_packets_share_progmem_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Equal constant packets share one table; lambdas stay templates."""
|
||||
"""Equal constant packets share one table; a lambda is sent from its vector."""
|
||||
main_cpp = generate_main(component_config_path("packet_tables.yaml"))
|
||||
|
||||
tables = dict(
|
||||
@@ -26,8 +26,10 @@ def test_constant_packets_share_progmem_tables(
|
||||
["{0xC5, 0x51, 0x78, 0x82}", "{0x68, 0x69}"]
|
||||
)
|
||||
shared = next(k for k, v in tables.items() if v == "{0xC5, 0x51, 0x78, 0x82}")
|
||||
assert main_cpp.count(f"set_data_static({shared}, 4);") == 2
|
||||
assert "set_data_template(" in main_cpp
|
||||
assert main_cpp.count(f"->transmit_packet(::{shared}, 4);") == 2
|
||||
assert (
|
||||
"->transmit_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_packet_data_length_limit() -> None:
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
on_boot:
|
||||
then:
|
||||
- uart.write: [0x01, 0x02, 0x03]
|
||||
- uart.write: !lambda return {0x09};
|
||||
- uart.write: ""
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
uart:
|
||||
tx_pin: GPIO1
|
||||
rx_pin: GPIO3
|
||||
baud_rate: 9600
|
||||
|
||||
switch:
|
||||
- platform: uart
|
||||
id: shared_switch
|
||||
name: Shared Switch
|
||||
data: [0x01, 0x02, 0x03]
|
||||
- platform: uart
|
||||
id: on_off_switch
|
||||
name: On Off Switch
|
||||
data:
|
||||
turn_on: "ON"
|
||||
turn_off: [0x04]
|
||||
|
||||
button:
|
||||
- platform: uart
|
||||
id: shared_button
|
||||
name: Shared Button
|
||||
data: [0x01, 0x02, 0x03]
|
||||
@@ -23,6 +23,19 @@ def test_constant_payloads_share_progmem_tables(
|
||||
)
|
||||
assert f"shared_switch->set_data_on({shared}, 3);" in main_cpp
|
||||
assert f"shared_button->set_data({shared}, 3);" in main_cpp
|
||||
assert f"set_data_static({shared}, 3);" in main_cpp
|
||||
assert "set_data_static(nullptr, 0);" in main_cpp
|
||||
assert "set_data_template(" in main_cpp
|
||||
assert f"->write_array(::{shared}, 3);" in main_cpp
|
||||
assert "->write_array(nullptr, 0);" in main_cpp
|
||||
assert "->write_array(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
|
||||
|
||||
def test_esp8266_write_action_reads_flash_directly(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""On ESP8266 the write action hands the table to write_array_progmem instead of copying it."""
|
||||
main_cpp = generate_main(component_config_path("payload_tables_esp8266.yaml"))
|
||||
|
||||
assert "->write_array_progmem(::uart_data, 3);" in main_cpp
|
||||
assert "->write_array_progmem(nullptr, 0);" in main_cpp
|
||||
assert "->write_array(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
assert "progmem_memcpy" not in main_cpp
|
||||
@@ -8,10 +8,17 @@ def test_write_payloads_share_a_progmem_table(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Identical constant payloads share one flash table; an empty one needs none."""
|
||||
"""Identical constant payloads share one flash table and are copied out of it on
|
||||
ESP8266 before sending; an empty one needs none; a lambda is sent from its vector."""
|
||||
main_cpp = generate_main(component_config_path("udp_write.yaml"))
|
||||
|
||||
assert main_cpp.count("udp_data[] PROGMEM = {0xAA, 0x55, 0x01, 0x02};") == 1
|
||||
assert main_cpp.count("->set_data_static(udp_data, 4);") == 2
|
||||
assert "->set_data_static(nullptr, 0);" in main_cpp
|
||||
assert "->set_data_template(" in main_cpp
|
||||
table = "esphome__udp_write__bytes"
|
||||
assert main_cpp.count(f"{table}[] PROGMEM = {{0xAA, 0x55, 0x01, 0x02}};") == 1
|
||||
assert (
|
||||
main_cpp.count(f"esphome::progmem_memcpy(esphome__bytes, ::{table}, 4);") == 2
|
||||
)
|
||||
assert main_cpp.count("->send_packet(esphome__bytes, 4);") == 2
|
||||
assert "uint8_t esphome__bytes[1];" in main_cpp
|
||||
assert "->send_packet(esphome__bytes, 0);" in main_cpp
|
||||
assert "->send_packet(esphome__bytes.data(), esphome__bytes.size());" in main_cpp
|
||||
assert main_cpp.count("->set_should_broadcast();") == 4
|
||||
@@ -0,0 +1,23 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
static_assert(bit_at(uint8_t{0b1010'0001}, 0));
|
||||
static_assert(!bit_at(uint8_t{0b1010'0001}, 1));
|
||||
static_assert(bit_at(uint8_t{0b1010'0001}, 7));
|
||||
static_assert(bit_at(uint64_t{1} << 63, 63));
|
||||
static_assert(!bit_at(uint64_t{1} << 63, 62));
|
||||
static_assert(bit_at(int32_t{-1}, 31));
|
||||
static_assert(!bit_at(int32_t{0x7FFF'FFFF}, 31));
|
||||
|
||||
TEST(BitAt, IsTheReadSideOfAssignBits) {
|
||||
uint16_t reg = 0;
|
||||
assign_bits(reg, 1 << 9, true);
|
||||
EXPECT_TRUE(bit_at(reg, 9));
|
||||
assign_bits(reg, 1 << 9, false);
|
||||
EXPECT_FALSE(bit_at(reg, 9));
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,36 @@
|
||||
# voice_assistant over Thread without the IPv4 stack
|
||||
packages:
|
||||
i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-c6-idf.yaml
|
||||
|
||||
openthread:
|
||||
channel: 13
|
||||
network_name: OpenThread-8f28
|
||||
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
|
||||
pan_id: 0x8f28
|
||||
ext_pan_id: 0xd63e8e3e495ebbc3
|
||||
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
|
||||
mesh_local_prefix: fd53:145f:ed22:ad81::/64
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
enable_ipv4: false
|
||||
|
||||
api:
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: va_mic
|
||||
i2s_audio_id: i2s_audio_bus
|
||||
i2s_din_pin: GPIO7
|
||||
adc_type: external
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: va_speaker
|
||||
i2s_audio_id: i2s_audio_bus
|
||||
dac_type: external
|
||||
i2s_dout_pin: GPIO10
|
||||
|
||||
voice_assistant:
|
||||
microphone: va_mic
|
||||
speaker: va_speaker
|
||||
@@ -11,3 +11,6 @@ openthread:
|
||||
output_power: 1dBm
|
||||
core_options:
|
||||
OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT: "5"
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
@@ -0,0 +1,14 @@
|
||||
# Common I2S audio bus configuration for ESP32-C6 IDF tests
|
||||
# Provides a shared i2s_audio bus that speaker/microphone components can use
|
||||
# Each consumer must give its speaker/microphone a unique data pin
|
||||
|
||||
substitutions:
|
||||
i2s_bclk_pin: GPIO4
|
||||
i2s_lrclk_pin: GPIO5
|
||||
i2s_mclk_pin: GPIO6
|
||||
|
||||
i2s_audio:
|
||||
- id: i2s_audio_bus
|
||||
i2s_bclk_pin: ${i2s_bclk_pin}
|
||||
i2s_lrclk_pin: ${i2s_lrclk_pin}
|
||||
i2s_mclk_pin: ${i2s_mclk_pin}
|
||||
@@ -236,7 +236,7 @@ def wisp_core(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest) -
|
||||
CORE.data,
|
||||
KEY_CORE,
|
||||
{
|
||||
KEY_TARGET_PLATFORM: Platform.ESP32,
|
||||
KEY_TARGET_PLATFORM: params.get("platform", Platform.ESP32),
|
||||
KEY_TARGET_FRAMEWORK: params.get("framework", "esp-idf"),
|
||||
KEY_FRAMEWORK_VERSION: params.get("idf", cv.Version(5, 4, 0)),
|
||||
},
|
||||
@@ -312,10 +312,14 @@ def test_second_dns_server_requires_idf_6(wisp_core: None, valid: bool) -> None:
|
||||
wifi_network_ap(ap)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wisp_core", [{"framework": "arduino"}], indirect=True)
|
||||
def test_wisp_requires_esp_idf(wisp_core: None) -> None:
|
||||
"""NAT is only available with the ESP-IDF framework."""
|
||||
with pytest.raises(cv.Invalid, match="only available with framework"):
|
||||
@pytest.mark.parametrize(
|
||||
"wisp_core",
|
||||
[{"platform": Platform.ESP8266, "framework": "arduino"}],
|
||||
indirect=True,
|
||||
)
|
||||
def test_wisp_requires_esp32(wisp_core: None) -> None:
|
||||
"""The bridge is part of the ESP32 backend; other platforms have no NAT."""
|
||||
with pytest.raises(cv.Invalid, match="only available on .*ESP32"):
|
||||
wifi_network_ap(_wisp_ap())
|
||||
|
||||
|
||||
|
||||
@@ -1536,13 +1536,87 @@ async def test_sub_entities_apply_errors(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sub_entities_apply_needs_parent(
|
||||
async def test_sub_entities_apply_without_parent_needs_namespaced_call(
|
||||
setup_test_environment: list[str],
|
||||
apply_functions: list[tuple[str, str, str]],
|
||||
) -> None:
|
||||
"""A hub method with no parent bound fails in Python, not as an undeclared name in C++."""
|
||||
calls: list[tuple[Any, ...]] = []
|
||||
conf = _apply_conf("k_id", _BUTTON_APPLY.cls)
|
||||
sub = _apply_sub(calls, {"k": conf}, _BUTTON_APPLY, parent=None)
|
||||
with pytest.raises(ValueError, match="namespaced free function"):
|
||||
await sub("k", None, apply="restart()")
|
||||
assert calls == []
|
||||
assert apply_functions == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("apply_entity", "type_", "publish", "params", "body"),
|
||||
[
|
||||
(
|
||||
_BUTTON_APPLY,
|
||||
_BUTTON_APPLY.cls,
|
||||
None,
|
||||
"button::Button * entity",
|
||||
"restart::restart_device();",
|
||||
),
|
||||
(
|
||||
_SWITCH_APPLY,
|
||||
_APPLY_SWITCH_CLS,
|
||||
"after",
|
||||
"switch_::Switch * entity, bool state",
|
||||
"restart::restart_device();\nentity->publish_state(state);",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_sub_entities_apply_without_parent(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
setup_test_environment: list[str],
|
||||
apply_functions: list[tuple[str, str, str]],
|
||||
apply_entity: entity_helpers.ApplyEntity,
|
||||
type_: MockObjClass,
|
||||
publish: str | None,
|
||||
params: str,
|
||||
body: str,
|
||||
) -> None:
|
||||
"""Without a parent the statement runs as written, with no parent lookup or deref."""
|
||||
|
||||
async def fail(*args: Any) -> None:
|
||||
raise AssertionError("must not be called")
|
||||
|
||||
monkeypatch.setattr(entity_helpers.cg, "register_parented", fail)
|
||||
monkeypatch.setattr(entity_helpers, "get_variable", fail)
|
||||
calls: list[tuple[Any, ...]] = []
|
||||
sub = _apply_sub(calls, {}, apply_entity, parent=None)
|
||||
var = await sub.create(
|
||||
_apply_conf("rst", type_),
|
||||
None,
|
||||
apply="restart::restart_device()",
|
||||
publish=publish,
|
||||
)
|
||||
assert str(var) == "rst"
|
||||
assert calls == [("rst", "<esphome__rst__fn>")]
|
||||
assert apply_functions == [("esphome__rst__fn", params, body)]
|
||||
assert setup_test_environment == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sub_entities_apply_without_parent_shares_equal_functions(
|
||||
setup_test_environment: list[str], apply_functions: list[tuple[str, str, str]]
|
||||
) -> None:
|
||||
"""Parentless entities with the same statement share one function."""
|
||||
calls: list[tuple[Any, ...]] = []
|
||||
sub = _apply_sub(calls, {"k": _apply_conf("k_id")}, parent=None)
|
||||
with pytest.raises(ValueError, match="needs a parent"):
|
||||
await sub("k", None, apply="x()")
|
||||
sub = _apply_sub(calls, {}, _BUTTON_APPLY, parent=None)
|
||||
for name in ("a_btn", "b_btn"):
|
||||
await sub.create(_apply_conf(name, _BUTTON_APPLY.cls), None, apply="x::go()")
|
||||
await sub.create(_apply_conf("c_btn", _BUTTON_APPLY.cls), None, apply="x::stop()")
|
||||
assert calls == [
|
||||
("a_btn", "<esphome__a_btn__fn>"),
|
||||
("b_btn", "<esphome__a_btn__fn>"),
|
||||
("c_btn", "<esphome__c_btn__fn>"),
|
||||
]
|
||||
assert [body for _, _, body in apply_functions] == ["x::go();", "x::stop();"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1319,25 +1319,52 @@ async def test_apply_string_ref_literal_stays_in_flash_on_esp8266(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("platform", ["esp32", "esp8266"])
|
||||
@pytest.mark.parametrize(
|
||||
"body", ["x *= 2;\nreturn {0x01, x};", "return std::vector<uint8_t>{++x};"]
|
||||
)
|
||||
async def test_apply_bytes_lambda_is_a_vector_in_a_block(
|
||||
registries: tuple[Registry, Registry], mock_cg: MockCodegen, platform: str
|
||||
registries: tuple[Registry, Registry],
|
||||
mock_cg: MockCodegen,
|
||||
platform: str,
|
||||
body: str,
|
||||
) -> None:
|
||||
"""A lambda payload is called once into a scoped vector and passed as data and size."""
|
||||
"""A lambda payload is called once into a scoped vector and passed as data and size;
|
||||
it takes the trigger args by their own types and is never reduced, so it may modify them."""
|
||||
fields = (ApplyBytes("data", "send({})"),)
|
||||
config = {"data": Lambda("return {0x01, x};")}
|
||||
config = {"data": Lambda(body)}
|
||||
await _run_apply_action(
|
||||
registries, fields, config, args=[(cg.uint8, "x")], platform=platform
|
||||
)
|
||||
text = _apply_definition(mock_cg)
|
||||
# Parenthesized so a reduced `return a, b;` stays one initializer
|
||||
assert "const std::vector<uint8_t> esphome__bytes = (" in text
|
||||
assert "-> std::vector<uint8_t>" in text
|
||||
assert text.startswith(
|
||||
"static void esphome__obj_1__fn(const std::remove_cvref_t<uint8_t> & x) {"
|
||||
)
|
||||
assert (
|
||||
"const std::vector<uint8_t> esphome__bytes = [](uint8_t x) -> std::vector<uint8_t> {"
|
||||
in text
|
||||
)
|
||||
assert "}(x);" in text
|
||||
assert (
|
||||
f"::{PARENT_OBJ}->send(esphome__bytes.data(), esphome__bytes.size());" in text
|
||||
)
|
||||
assert "progmem_memcpy" not in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_bytes_keeps_a_mutable_reference_arg(
|
||||
registries: tuple[Registry, Registry], mock_cg: MockCodegen
|
||||
) -> None:
|
||||
"""A reference trigger arg such as climate's `ClimateCall &` is passed through as is."""
|
||||
fields = (ApplyBytes("data", "send({})"),)
|
||||
config = {"data": Lambda("return {0x01};")}
|
||||
call_ref = cg.MockObj("ClimateCall").operator("ref")
|
||||
await _run_apply_action(registries, fields, config, args=[(call_ref, "x")])
|
||||
text = _apply_definition(mock_cg)
|
||||
assert text.startswith("static void esphome__obj_1__fn(ClimateCall & x) {")
|
||||
assert "[](ClimateCall & x) -> std::vector<uint8_t> {" in text
|
||||
assert "}(x);" in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_bytes_constant_is_a_shared_table(
|
||||
registries: tuple[Registry, Registry], mock_cg: MockCodegen
|
||||
|
||||
Reference in new issue
Block a user