Compare commits

..
Author SHA1 Message Date
4242bcb084 [vnc] Add the Hextile encoding and skip unchanged tiles (#20625)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-22 21:52:53 +00:00
J. Nick Koston 8a88e97497 [weikai] Share print_buffer between the SPI and I2C drivers (#20721) 2026-10-22 21:21:41 +00:00
Frédéric MetrichandClaude d586e20a31 [teleinfo] Fix default units of unitless, text and PMAX tags (#21367)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 17:21:09 -04:00
e6161e64b1 [rx8111] Add alarm and wake-up timer support (#21217)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-22 21:14:37 +00:00
J. Nick Koston 431843c27e [tests] Use LineWaiter for the integration tests that parse or count log lines (#21306) 2026-10-22 11:14:34 -10:00
J. Nick Koston a1da3d6b90 [unicode] Stop reading past a truncated UTF-8 sequence (#21374) 2026-10-22 11:14:17 -10:00
J. Nick Koston 930429061a [tests] Use LineWaiter for the integration tests that only wait on log lines (#21305) 2026-10-22 11:14:01 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] afceb91d90 [tests] Use wait_or_fail for the remaining integration test timeouts (#21304)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-22 11:13:55 -10:00
J. Nick Koston 7d741361a3 [weikai] Keep the register name tables in flash (#20712) 2026-10-22 11:13:04 -10:00
miloit 9c70ffd826 [ds3231] Add time platform, alarms, binary sensors and output control (#21054) 2026-10-22 10:58:28 -10:00
J. Nick Koston dac6a9c8cc [cover] Pack the cover traits flags into one byte (#21339) 2026-10-22 16:53:20 -04:00
J. Nick Koston a3a4144e4f [valve] Pack the valve traits flags into one byte (#21343) 2026-10-22 16:52:35 -04:00
J. Nick Koston 29e9ae008d [lock] Pack the lock traits flags into one byte (#21344) 2026-10-22 16:52:05 -04:00
Bonne Eggleston c92c4d7f99 [deep_sleep] Add woke_from_deep_sleep() and build get_wakeup_cause() without on_wake (#21425) 2026-10-22 10:49:15 -10:00
5bcc3f42e9 [switch] Add initial_state and restore_state, with flash or RTC storage (#20978)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-22 10:35:23 -10:00
Jonathan Swoboda d6aa5ba14c [esp32] Share one validator for options that need a newer ESP-IDF (#21422) 2026-10-22 10:20:22 -10:00
Clyde StubbsandClaude Opus 5.5 59c7178910 [runtime_image] Decode JPEG a step at a time (#21341)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 06:52:41 +11:00
J. Nick Koston e55741a26b [usb_uart] Read the channel limit when the config is validated, not at import (#21331) 2026-10-22 09:34:55 -10:00
dependabot[bot] 0bdc14154b Bump prek from 0.5.5 to 0.5.6 (#21419)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:52 +00:00
dependabot[bot] 1d6f839ee7 Bump platformdirs from 4.13.4 to 4.13.5 (#21418)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:20 +00:00
dependabot[bot] 2544518aca Bump filelock from 4.2.2 to 4.2.4 (#21417)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:17 +00:00
dependabot[bot] 70e56df630 Bump j178/prek-action from 3.0.1 to 3.0.2 (#21420)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:29:54 +00:00
Jonathan Swoboda 7d62aae996 [wifi] Allow the lease cache and a second WISP DNS server on the ESP32 Arduino framework (#21412) 2026-10-22 15:27:16 -04:00
c12baaa337 [mqtt] Support stop tilt on covers (#20251)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-22 19:14:24 +00:00
Jonathan Swoboda cb9eda3681 [spi] Gate psram_dma on the ESP-IDF version rather than the framework (#21411) 2026-10-22 14:48:04 -04:00
J. Nick Koston 4e27957c74 [ci] Add the UART loopback, WISP bridge, raw API action and LD2412 flows to the hardware-test skill (#21356) 2026-10-22 08:46:37 -10:00
Roman PriesolandJ. Nick Koston 0856211b9b [voice_assistant] Allow IPv4 to be disabled (#21389)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-22 08:46:26 -10:00
J. Nick Koston ff4ff5c5f5 [restart][shutdown][factory_reset][safe_mode] Use Apply buttons (#21158) 2026-10-22 08:37:53 -10:00
J. Nick Koston 980b336668 [core] Let Apply entities run a free statement without a parent hub (#21157) 2026-10-22 08:37:52 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] a3cb3732ee [sx127x][sx126x][cc1101][udp][uart] Send byte payloads with ApplyBytes (#21263)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-22 08:19:09 -10:00
Jonathan Swoboda ab50b3830a [wifi] Allow WISP mode on the ESP32 Arduino framework (#21410) 2026-10-22 18:10:47 +00:00
Roman Priesol 3131cf28a3 [esphome] Fix OTA build with IPv4 disabled (#21388) 2026-10-22 08:07:17 -10:00
260 changed files with 9248 additions and 3705 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -286,7 +286,7 @@ jobs:
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Run prek
uses: j178/prek-action@1382ad72dea72f9d53832f2bcb65b3e87a947204 # v3.0.1
uses: j178/prek-action@af4d4fcb6922b8e6f65c0426947125fcc38794ce # v3.0.2
with:
prek-version: ${{ steps.prek.outputs.version }}
# This job only runs on pull requests, so nothing ever populates
+2
View File
@@ -446,6 +446,8 @@ file does, and it is the authority when they disagree. The most useful starting
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
```
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
to still drive a state when the restore mode is disabled.
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
+26 -11
View File
@@ -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}')}", "}"]
+2 -19
View File
@@ -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()}
-9
View File
@@ -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
+5 -2
View File
@@ -1,5 +1,4 @@
#include "ch422g.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ch422g {
@@ -60,7 +59,11 @@ bool CH422GComponent::digital_read(uint8_t pin) {
}
void CH422GComponent::digital_write(uint8_t pin, bool value) {
assign_bits(this->output_bits_, 1 << pin, value);
if (value) {
this->output_bits_ |= (1 << pin);
} else {
this->output_bits_ &= ~(1 << pin);
}
this->write_outputs_();
}
+5 -2
View File
@@ -1,5 +1,4 @@
#include "ch423.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -63,7 +62,11 @@ bool CH423Component::digital_read(uint8_t pin) {
}
void CH423Component::digital_write(uint8_t pin, bool value) {
assign_bits(this->output_bits_, 1 << pin, value);
if (value) {
this->output_bits_ |= (1 << pin);
} else {
this->output_bits_ &= ~(1 << pin);
}
this->write_outputs_();
}
+1 -4
View File
@@ -8,10 +8,7 @@ ESPHOME_LOG_TAG(TAG, "copy.lock");
void CopyLock::setup() {
source_->add_on_state_callback([this](lock::LockState state) { this->publish_state(state); });
traits.set_assumed_state(source_->traits.get_assumed_state());
traits.set_requires_code(source_->traits.get_requires_code());
traits.set_supported_states_mask(source_->traits.get_supported_states_mask());
traits.set_supports_open(source_->traits.get_supports_open());
this->traits = this->source_->traits;
this->publish_state(source_->state);
}
+24 -18
View File
@@ -1,31 +1,37 @@
#pragma once
#include <cstdint>
namespace esphome::cover {
class CoverTraits {
public:
CoverTraits() = default;
bool get_is_assumed_state() const { return this->is_assumed_state_; }
void set_is_assumed_state(bool is_assumed_state) { this->is_assumed_state_ = is_assumed_state; }
bool get_supports_position() const { return this->supports_position_; }
void set_supports_position(bool supports_position) { this->supports_position_ = supports_position; }
bool get_supports_tilt() const { return this->supports_tilt_; }
void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; }
bool get_supports_toggle() const { return this->supports_toggle_; }
void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; }
bool get_supports_stop() const { return this->supports_stop_; }
void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; }
bool get_supports_stop_tilt() const { return this->supports_stop_tilt_; }
void set_supports_stop_tilt(bool supports_stop_tilt) { this->supports_stop_tilt_ = supports_stop_tilt; }
bool get_is_assumed_state() const { return this->flags_.is_assumed_state; }
void set_is_assumed_state(bool is_assumed_state) { this->flags_.is_assumed_state = is_assumed_state; }
bool get_supports_position() const { return this->flags_.supports_position; }
void set_supports_position(bool supports_position) { this->flags_.supports_position = supports_position; }
bool get_supports_tilt() const { return this->flags_.supports_tilt; }
void set_supports_tilt(bool supports_tilt) { this->flags_.supports_tilt = supports_tilt; }
bool get_supports_toggle() const { return this->flags_.supports_toggle; }
void set_supports_toggle(bool supports_toggle) { this->flags_.supports_toggle = supports_toggle; }
bool get_supports_stop() const { return this->flags_.supports_stop; }
void set_supports_stop(bool supports_stop) { this->flags_.supports_stop = supports_stop; }
bool get_supports_stop_tilt() const { return this->flags_.supports_stop_tilt; }
void set_supports_stop_tilt(bool supports_stop_tilt) { this->flags_.supports_stop_tilt = supports_stop_tilt; }
protected:
bool is_assumed_state_{false};
bool supports_position_{false};
bool supports_tilt_{false};
bool supports_toggle_{false};
bool supports_stop_{false};
bool supports_stop_tilt_{false};
// Bit-packed flags to save memory (1 byte instead of 6)
struct CoverTraitFlags {
uint8_t is_assumed_state : 1;
uint8_t supports_position : 1;
uint8_t supports_tilt : 1;
uint8_t supports_toggle : 1;
uint8_t supports_stop : 1;
uint8_t supports_stop_tilt : 1;
uint8_t reserved : 2;
} flags_{};
};
} // namespace esphome::cover
@@ -7,7 +7,6 @@ namespace esphome::deep_sleep {
ESPHOME_LOG_TAG(TAG, "deep_sleep");
#ifdef USE_DEEP_SLEEP_ON_WAKE
WakeupCause get_wakeup_cause() {
switch (lt_get_reboot_reason()) {
case REBOOT_REASON_SLEEP_GPIO:
@@ -20,7 +19,6 @@ WakeupCause get_wakeup_cause() {
return WAKEUP_CAUSE_NONE;
}
}
#endif // USE_DEEP_SLEEP_ON_WAKE
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
@@ -60,8 +60,6 @@ struct WakeupCauseToRunDuration {
#endif // USE_ESP32
#ifdef USE_DEEP_SLEEP_ON_WAKE
/// Why the device woke from deep sleep. Passed to on_wake automations.
enum WakeupCause : uint8_t {
/// The device did not wake from deep sleep (for example a cold boot, reset or OTA restart).
@@ -79,6 +77,11 @@ enum WakeupCause : uint8_t {
/// Return why the device woke from deep sleep. Implemented per platform.
WakeupCause get_wakeup_cause();
/// Return true when this boot is a wake from deep sleep.
inline bool woke_from_deep_sleep() { return get_wakeup_cause() != WAKEUP_CAUSE_NONE; }
#ifdef USE_DEEP_SLEEP_ON_WAKE
/** Setup priority of on_wake triggers.
*
* Between restoring global variables (setup_priority::HARDWARE, 800) and on_boot automations at
@@ -30,7 +30,6 @@ namespace esphome::deep_sleep {
ESPHOME_LOG_TAG(TAG, "deep_sleep");
#ifdef USE_DEEP_SLEEP_ON_WAKE
WakeupCause get_wakeup_cause() {
switch (esp_sleep_get_wakeup_cause()) {
case ESP_SLEEP_WAKEUP_EXT0:
@@ -47,7 +46,6 @@ WakeupCause get_wakeup_cause() {
return WAKEUP_CAUSE_UNKNOWN;
}
}
#endif // USE_DEEP_SLEEP_ON_WAKE
optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
if (this->wakeup_cause_to_run_duration_.has_value()) {
@@ -3,17 +3,14 @@
#include <Esp.h>
#ifdef USE_DEEP_SLEEP_ON_WAKE
extern "C" {
#include <user_interface.h>
}
#endif
namespace esphome::deep_sleep {
ESPHOME_LOG_TAG(TAG, "deep_sleep");
#ifdef USE_DEEP_SLEEP_ON_WAKE
WakeupCause get_wakeup_cause() {
// The ESP8266 can only wake from deep sleep through the RTC timer (via GPIO16 -> RST).
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
@@ -22,7 +19,6 @@ WakeupCause get_wakeup_cause() {
}
return WAKEUP_CAUSE_NONE;
}
#endif // USE_DEEP_SLEEP_ON_WAKE
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
@@ -31,6 +31,9 @@ static bool wakeable_delay_feed_wdt(uint32_t ms) {
return false;
}
// A wake from System OFF is not told apart from a reset yet.
WakeupCause get_wakeup_cause() { return WAKEUP_CAUSE_NONE; }
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {}
+239 -6
View File
@@ -1,7 +1,18 @@
from esphome import automation
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.const import CONF_ENABLED
import esphome.config_validation as cv
from esphome.const import (
CONF_FREQUENCY,
CONF_HOUR,
CONF_INTERRUPT_PIN,
CONF_MINUTE,
CONF_MODE,
CONF_OFFSET,
CONF_SECOND,
CONF_STATE,
)
from esphome.types import ConfigType
CODEOWNERS = ["@linkedupbits"]
@@ -10,23 +21,245 @@ MULTI_CONF = True
DOMAIN = "ds3231"
CONF_DS3231_ID = "ds3231_id"
CONF_ALARM = "alarm"
CONF_ALARM_CHECK_INTERVAL = "alarm_check_interval"
CONF_AGING_OFFSET = "aging_offset"
CONF_AUTO_CLEAR_ALARMS = "auto_clear_alarms"
CONF_BATTERY_BACKED_SQUARE_WAVE = "battery_backed_square_wave"
CONF_DAY_OF_MONTH = "day_of_month"
CONF_DAY_OF_WEEK = "day_of_week"
CONF_ON_ALARM_1 = "on_alarm_1"
CONF_ON_ALARM_2 = "on_alarm_2"
CONF_OUTPUT_32KHZ = "output_32khz"
CONF_SQUARE_WAVE_FREQUENCY = "square_wave_frequency"
ds3231_ns = cg.esphome_ns.namespace("ds3231")
DS3231Component = ds3231_ns.class_("DS3231Component", cg.Component, i2c.I2CDevice)
CONFIG_SCHEMA = (
cv.Schema({cv.GenerateID(): cv.declare_id(DS3231Component)})
AlarmMode = ds3231_ns.enum("AlarmMode")
ALARM_1_MODES = AlarmMode.options(
"every_second",
"every_minute",
"hourly",
"daily",
"monthly",
"weekly",
prefix="ALARM_MODE_",
)
# Alarm 2 has no seconds register.
ALARM_2_MODES = {k: v for k, v in ALARM_1_MODES.items() if k != "every_second"}
SquareWaveFrequency = ds3231_ns.enum("SquareWaveFrequency")
# Keyed by the frequency in Hz; cv.frequency turns "1024Hz", "1.024kHz" or 1024 into it.
SQUARE_WAVE_FREQUENCIES = {
1: SquareWaveFrequency.SQUARE_WAVE_1HZ,
1024: SquareWaveFrequency.SQUARE_WAVE_1024HZ,
4096: SquareWaveFrequency.SQUARE_WAVE_4096HZ,
8192: SquareWaveFrequency.SQUARE_WAVE_8192HZ,
}
validate_square_wave_frequency = cv.All(
cv.frequency, cv.enum(SQUARE_WAVE_FREQUENCIES, int=True)
)
validate_aging_offset = cv.int_range(min=-128, max=127)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DS3231Component),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_SQUARE_WAVE_FREQUENCY): validate_square_wave_frequency,
cv.Optional(CONF_BATTERY_BACKED_SQUARE_WAVE): cv.boolean,
cv.Optional(CONF_OUTPUT_32KHZ): cv.boolean,
cv.Optional(CONF_AGING_OFFSET): validate_aging_offset,
cv.Optional(CONF_AUTO_CLEAR_ALARMS, default=True): cv.boolean,
cv.Optional(
CONF_ALARM_CHECK_INTERVAL, default="1s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_ALARM_1): automation.validate_automation(single=True),
cv.Optional(CONF_ON_ALARM_2): automation.validate_automation(single=True),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x68))
.extend(i2c.i2c_device_schema(0x68)),
cv.has_at_most_one_key(CONF_INTERRUPT_PIN, CONF_SQUARE_WAVE_FREQUENCY),
)
async def to_code(config: ConfigType) -> None:
await i2c.new_i2c_component(config)
var = await i2c.new_i2c_component(config)
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
cg.add_optional(
var.set_square_wave_frequency, config.get(CONF_SQUARE_WAVE_FREQUENCY)
)
cg.add_optional(
var.set_battery_backed_square_wave, config.get(CONF_BATTERY_BACKED_SQUARE_WAVE)
)
cg.add_optional(var.set_output_32khz_config, config.get(CONF_OUTPUT_32KHZ))
cg.add_optional(var.set_aging_offset_config, config.get(CONF_AGING_OFFSET))
cg.add(var.set_auto_clear_alarms(config[CONF_AUTO_CLEAR_ALARMS]))
cg.add(var.set_alarm_check_interval(config[CONF_ALARM_CHECK_INTERVAL]))
# The alarm flags are only read when something uses them; the binary sensor platform
# turns this on as well. With an interrupt pin the check also releases INT/SQW.
if any(
key in config for key in (CONF_INTERRUPT_PIN, CONF_ON_ALARM_1, CONF_ON_ALARM_2)
):
cg.add(var.set_monitor_alarms(True))
# One callback for both alarms; each automation runs only for its own alarm number.
for conf_key, alarm in ((CONF_ON_ALARM_1, 1), (CONF_ON_ALARM_2, 2)):
if (conf := config.get(conf_key)) is not None:
await automation.build_callback_automation(
var,
"add_on_alarm_callback",
[],
conf,
params=[(cg.uint8, "alarm")],
forward=[],
when=f"alarm == {alarm}",
)
# ---------------------------------------------------------------------------------------------
# Actions
# ---------------------------------------------------------------------------------------------
DS3231_ACTION_SCHEMA = automation.maybe_simple_id(
{cv.GenerateID(): cv.use_id(DS3231Component)}
)
automation.register_apply_action(
"ds3231.force_temperature_conversion",
automation.maybe_simple_id({cv.GenerateID(): cv.use_id(DS3231Component)}),
DS3231_ACTION_SCHEMA,
automation.ApplyCall("force_temperature_conversion()"),
)
def _validate_alarm_day(config: ConfigType) -> ConfigType:
mode = config[CONF_MODE]
if mode == "monthly" and CONF_DAY_OF_MONTH not in config:
raise cv.Invalid(f"'{CONF_DAY_OF_MONTH}' is required in monthly mode")
if mode == "weekly" and CONF_DAY_OF_WEEK not in config:
raise cv.Invalid(f"'{CONF_DAY_OF_WEEK}' is required in weekly mode")
# The unused day still has to be a valid register value.
config.setdefault(CONF_DAY_OF_MONTH, 1)
config.setdefault(CONF_DAY_OF_WEEK, 1)
return config
def _alarm_schema(modes: dict, with_second: bool) -> cv.Schema:
schema = {
cv.GenerateID(): cv.use_id(DS3231Component),
cv.Required(CONF_MODE): cv.enum(modes, lower=True),
cv.Optional(CONF_MINUTE, default=0): cv.templatable(
cv.int_range(min=0, max=59)
),
cv.Optional(CONF_HOUR, default=0): cv.templatable(cv.int_range(min=0, max=23)),
cv.Optional(CONF_DAY_OF_MONTH): cv.templatable(cv.int_range(min=1, max=31)),
# 1 = Sunday ... 7 = Saturday, as in ESPHome's time component.
cv.Optional(CONF_DAY_OF_WEEK): cv.templatable(cv.int_range(min=1, max=7)),
cv.Optional(CONF_ENABLED, default=True): cv.templatable(cv.boolean),
}
if with_second:
schema[cv.Optional(CONF_SECOND, default=0)] = cv.templatable(
cv.int_range(min=0, max=59)
)
return cv.All(cv.Schema(schema), _validate_alarm_day)
automation.register_apply_action(
"ds3231.set_alarm_1",
_alarm_schema(ALARM_1_MODES, with_second=True),
automation.ApplyCall(
"set_alarm_1({}, {}, {}, {}, {}, {}, {})",
(
(CONF_MODE, AlarmMode),
(CONF_SECOND, cg.uint8),
(CONF_MINUTE, cg.uint8),
(CONF_HOUR, cg.uint8),
(CONF_DAY_OF_MONTH, cg.uint8),
(CONF_DAY_OF_WEEK, cg.uint8),
(CONF_ENABLED, cg.bool_),
),
),
)
automation.register_apply_action(
"ds3231.set_alarm_2",
_alarm_schema(ALARM_2_MODES, with_second=False),
automation.ApplyCall(
"set_alarm_2({}, {}, {}, {}, {}, {})",
(
(CONF_MODE, AlarmMode),
(CONF_MINUTE, cg.uint8),
(CONF_HOUR, cg.uint8),
(CONF_DAY_OF_MONTH, cg.uint8),
(CONF_DAY_OF_WEEK, cg.uint8),
(CONF_ENABLED, cg.bool_),
),
),
)
DS3231_ALARM_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(DS3231Component),
cv.Required(CONF_ALARM): cv.templatable(cv.int_range(min=1, max=2)),
}
)
for _name, _method in (
("ds3231.enable_alarm", "enable_alarm"),
("ds3231.disable_alarm", "disable_alarm"),
("ds3231.clear_alarm", "clear_alarm"),
):
automation.register_apply_action(
_name,
DS3231_ALARM_ACTION_SCHEMA,
automation.ApplyField(CONF_ALARM, _method, cg.uint8),
)
automation.register_apply_condition(
"ds3231.is_alarm_triggered",
DS3231_ALARM_ACTION_SCHEMA,
automation.ApplyCall("is_alarm_triggered({})", ((CONF_ALARM, cg.uint8),)),
)
automation.register_apply_action(
"ds3231.enable_square_wave",
cv.Schema(
{
cv.GenerateID(): cv.use_id(DS3231Component),
cv.Required(CONF_FREQUENCY): validate_square_wave_frequency,
}
),
automation.ApplyField(CONF_FREQUENCY, "enable_square_wave", SquareWaveFrequency),
)
automation.register_apply_action(
"ds3231.disable_square_wave",
DS3231_ACTION_SCHEMA,
automation.ApplyCall("disable_square_wave()"),
)
automation.register_apply_action(
"ds3231.set_32khz_output",
cv.Schema(
{
cv.GenerateID(): cv.use_id(DS3231Component),
cv.Required(CONF_STATE): cv.templatable(cv.boolean),
}
),
automation.ApplyField(CONF_STATE, "set_output_32khz", cg.bool_),
)
automation.register_apply_action(
"ds3231.set_aging_offset",
cv.Schema(
{
cv.GenerateID(): cv.use_id(DS3231Component),
cv.Required(CONF_OFFSET): cv.templatable(validate_aging_offset),
}
),
automation.ApplyField(CONF_OFFSET, "set_aging_offset", cg.int8),
)
@@ -0,0 +1,47 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, DEVICE_CLASS_PROBLEM, ENTITY_CATEGORY_DIAGNOSTIC
from esphome.types import ConfigType
from .. import CONF_DS3231_ID, DS3231Component
DEPENDENCIES = ["ds3231"]
CONF_ALARM_1 = "alarm_1"
CONF_ALARM_2 = "alarm_2"
CONF_OSCILLATOR_STOPPED = "oscillator_stopped"
ICON_ALARM = "mdi:alarm"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DS3231_ID): cv.use_id(DS3231Component),
cv.Optional(CONF_ALARM_1): binary_sensor.binary_sensor_schema(
icon=ICON_ALARM
),
cv.Optional(CONF_ALARM_2): binary_sensor.binary_sensor_schema(
icon=ICON_ALARM
),
cv.Optional(CONF_OSCILLATOR_STOPPED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
),
cv.has_at_least_one_key(CONF_ALARM_1, CONF_ALARM_2, CONF_OSCILLATOR_STOPPED),
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DS3231_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_ALARM_1, hub.set_alarm_1_binary_sensor)
await binary_sensors(CONF_ALARM_2, hub.set_alarm_2_binary_sensor)
await binary_sensors(
CONF_OSCILLATOR_STOPPED, hub.set_oscillator_stopped_binary_sensor
)
if CONF_ALARM_1 in config or CONF_ALARM_2 in config:
cg.add(hub.set_monitor_alarms(True))
+423 -14
View File
@@ -9,29 +9,184 @@ namespace esphome::ds3231 {
ESPHOME_LOG_TAG(TAG, "ds3231");
static constexpr uint8_t DS3231_REG_SECONDS = 0x00;
static constexpr uint8_t DS3231_REG_ALARM_1 = 0x07;
static constexpr uint8_t DS3231_REG_ALARM_2 = 0x0B;
static constexpr uint8_t DS3231_REG_CONTROL = 0x0E;
static constexpr uint8_t DS3231_REG_STATUS = 0x0F;
static constexpr uint8_t DS3231_REG_AGING_OFFSET = 0x10;
static constexpr uint8_t DS3231_REG_TEMPERATURE = 0x11;
static constexpr uint8_t ALARM_MASK_BIT = 0x80; // AxMy: this register is ignored when matching
static constexpr uint8_t ALARM_DAY_OF_WEEK = 0x40; // DY/DT: match day of week instead of date
/// How long an alarm binary sensor stays on when the flag is cleared automatically.
static constexpr uint32_t ALARM_PULSE_MS = 1000;
static const LogString *alarm_state_to_string(uint8_t control, uint8_t alarm) {
return (control & (alarm == 1 ? 0x01 : 0x02)) != 0 ? LOG_STR("enabled") : LOG_STR("disabled");
}
static const LogString *square_wave_to_string(SquareWaveFrequency frequency) {
switch (frequency) {
case SQUARE_WAVE_1HZ:
return LOG_STR("1 Hz");
case SQUARE_WAVE_1024HZ:
return LOG_STR("1.024 kHz");
case SQUARE_WAVE_4096HZ:
return LOG_STR("4.096 kHz");
case SQUARE_WAVE_8192HZ:
return LOG_STR("8.192 kHz");
default:
return LOG_STR("unknown");
}
}
void IRAM_ATTR DS3231Component::interrupt_handler(DS3231Component *arg) { arg->enable_loop_soon_any_context(); }
void DS3231Component::setup() {
if (!this->read_control_status_()) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->control_reg_ &= ~CONTROL_EOSC; // keep the oscillator running on battery
// INT/SQW is the (unused) interrupt line rather than a square-wave output.
this->control_reg_ |= CONTROL_INTCN;
if (!this->write_control_()) {
// Keep the oscillator running on battery. INT/SQW is the alarm interrupt line unless a square
// wave is configured. The alarm interrupt enables (A1IE/A2IE) are kept as they are: the alarms
// live on the chip and survive a reboot of the ESP on battery.
uint8_t mask = CONTROL_EOSC | CONTROL_INTCN;
uint8_t value = CONTROL_INTCN;
if (this->square_wave_configured_) {
mask |= CONTROL_RS_MASK;
value = this->square_wave_frequency_ << CONTROL_RS_SHIFT;
}
if (this->battery_backed_square_wave_ >= 0) {
mask |= CONTROL_BBSQW;
assign_bits(value, CONTROL_BBSQW, this->battery_backed_square_wave_ != 0);
}
if (!this->update_control_(mask, value)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (this->output_32khz_ >= 0) {
assign_bits(this->status_reg_, STATUS_EN32KHZ, this->output_32khz_ != 0);
if (!this->write_status_(0)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
}
if (this->aging_offset_configured_ && !this->write_aging_offset_(this->aging_offset_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if ((this->status_reg_ & STATUS_OSF) != 0) {
ESP_LOGW(TAG, "Oscillator was stopped (battery empty or missing?), the stored time is invalid.");
}
this->publish_oscillator_stopped_();
if (!this->monitor_alarms_) {
this->disable_loop();
return;
}
// Start from a known state; an alarm that fired while the ESP was off is picked up by the first
// check, which runs from the loop right after setup.
this->publish_alarm_state_(1, false);
this->publish_alarm_state_(2, false);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
// INT/SQW is open drain and active low: a falling edge means an enabled alarm matched.
// The handler only wakes the loop; the flags are read from there.
this->interrupt_pin_->attach_interrupt(&DS3231Component::interrupt_handler, this, gpio::INTERRUPT_FALLING_EDGE);
}
// Without an interrupt pin this is how alarms are seen. With one it is a fallback: it picks up an
// edge that was missed, and latched flags (auto_clear_alarms: false) that were cleared elsewhere.
this->set_interval(this->alarm_check_interval_, [this]() { this->check_alarms_(); });
}
void DS3231Component::loop() {
// Only runs once after setup and when woken by the interrupt pin.
this->disable_loop();
this->check_alarms_();
}
void DS3231Component::dump_config() {
ESP_LOGCONFIG(TAG, "DS3231:");
LOG_I2C_DEVICE(this);
if (this->is_failed())
return;
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
if ((this->control_reg_ & CONTROL_INTCN) == 0) {
ESP_LOGCONFIG(TAG, " INT/SQW: square wave %s",
LOG_STR_ARG(square_wave_to_string(
static_cast<SquareWaveFrequency>((this->control_reg_ & CONTROL_RS_MASK) >> CONTROL_RS_SHIFT))));
} else {
ESP_LOGCONFIG(TAG, " INT/SQW: alarm interrupt");
}
ESP_LOGCONFIG(TAG,
" Battery-backed square wave: %s\n"
" 32 kHz output: %s\n"
" Alarm 1: %s\n"
" Alarm 2: %s\n"
" Auto clear alarms: %s",
YESNO(this->control_reg_ & CONTROL_BBSQW), ONOFF(this->status_reg_ & STATUS_EN32KHZ),
LOG_STR_ARG(alarm_state_to_string(this->control_reg_, 1)),
LOG_STR_ARG(alarm_state_to_string(this->control_reg_, 2)), YESNO(this->auto_clear_alarms_));
if (this->aging_offset_configured_) {
ESP_LOGCONFIG(TAG, " Aging offset: %d", this->aging_offset_);
}
if (this->monitor_alarms_) {
ESP_LOGCONFIG(TAG, " Alarm check interval: %" PRIu32 " ms", this->alarm_check_interval_);
}
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Alarm 1", this->alarm_1_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Alarm 2", this->alarm_2_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Oscillator Stopped", this->oscillator_stopped_binary_sensor_);
#endif
}
// ---------------------------------------------------------------------------------------------
// Clock
// ---------------------------------------------------------------------------------------------
bool DS3231Component::read_calendar(uint8_t *raw) {
if (this->is_failed() || !this->status_comm_result(this->read_bytes(DS3231_REG_SECONDS, raw, CALENDAR_LEN)))
return false;
// Without a valid status the oscillator-stop flag is unknown, so the time can't be trusted.
if (!this->read_control_status_())
return false;
this->publish_oscillator_stopped_();
if ((this->status_reg_ & STATUS_OSF) != 0) {
ESP_LOGW(TAG, "Oscillator stop flag set, the RTC time is invalid. Write a valid time first.");
return false;
}
return true;
}
bool DS3231Component::write_calendar(const uint8_t *raw) {
if (this->is_failed() || !this->status_comm_result(this->write_bytes(DS3231_REG_SECONDS, raw, CALENDAR_LEN)))
return false;
// The time is valid again: clear the oscillator stop flag. If that fails, the RTC would keep
// reporting its time as invalid, so the write is not reported as successful.
if (!this->read_control_status_())
return false;
if ((this->status_reg_ & STATUS_OSF) != 0) {
if (!this->write_status_(STATUS_OSF))
return false;
this->status_reg_ &= ~STATUS_OSF;
}
this->publish_oscillator_stopped_();
return true;
}
// ---------------------------------------------------------------------------------------------
// Temperature
// ---------------------------------------------------------------------------------------------
bool DS3231Component::read_temperature(float &out) {
uint8_t raw[2];
if (this->is_failed() || !this->read_bytes(DS3231_REG_TEMPERATURE, raw, sizeof(raw))) {
@@ -53,26 +208,280 @@ void DS3231Component::force_temperature_conversion() {
ESP_LOGW(TAG, "A temperature conversion is already in progress.");
return;
}
this->control_reg_ |= CONTROL_CONV;
this->write_control_();
// CONV is a one-shot command bit, so it is not kept in the cached value.
this->status_comm_result(this->write_byte(DS3231_REG_CONTROL, this->control_reg_ | CONTROL_CONV));
}
// ---------------------------------------------------------------------------------------------
// Alarms
// ---------------------------------------------------------------------------------------------
void DS3231Component::set_alarm_1(AlarmMode mode, uint8_t second, uint8_t minute, uint8_t hour, uint8_t day_of_month,
uint8_t day_of_week, bool enabled) {
if (second > 59 || minute > 59 || hour > 23 || day_of_month < 1 || day_of_month > 31 || day_of_week < 1 ||
day_of_week > 7 || mode > ALARM_MODE_WEEKLY) {
ESP_LOGE(TAG, "Invalid alarm 1 setting.");
return;
}
// Mask bits A1M1..A1M4: a set bit means the register is ignored when matching.
uint8_t regs[4] = {
uint8_to_bcd(second),
uint8_to_bcd(minute),
uint8_to_bcd(hour),
mode == ALARM_MODE_WEEKLY ? static_cast<uint8_t>(ALARM_DAY_OF_WEEK | day_of_week) : uint8_to_bcd(day_of_month),
};
switch (mode) {
case ALARM_MODE_EVERY_SECOND:
regs[0] |= ALARM_MASK_BIT;
[[fallthrough]];
case ALARM_MODE_EVERY_MINUTE:
regs[1] |= ALARM_MASK_BIT;
[[fallthrough]];
case ALARM_MODE_HOURLY:
regs[2] |= ALARM_MASK_BIT;
[[fallthrough]];
case ALARM_MODE_DAILY:
regs[3] |= ALARM_MASK_BIT;
break;
default: // MONTHLY / WEEKLY: everything matches
break;
}
ESP_LOGD(TAG, "Setting alarm 1: mode %u, %02u:%02u:%02u, day of month %u, day of week %u, %s", mode, hour, minute,
second, day_of_month, day_of_week, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
this->write_alarm_(1, regs, sizeof(regs), enabled);
}
void DS3231Component::set_alarm_2(AlarmMode mode, uint8_t minute, uint8_t hour, uint8_t day_of_month,
uint8_t day_of_week, bool enabled) {
if (minute > 59 || hour > 23 || day_of_month < 1 || day_of_month > 31 || day_of_week < 1 || day_of_week > 7 ||
mode == ALARM_MODE_EVERY_SECOND || mode > ALARM_MODE_WEEKLY) {
ESP_LOGE(TAG, "Invalid alarm 2 setting.");
return;
}
// Mask bits A2M2..A2M4.
uint8_t regs[3] = {
uint8_to_bcd(minute),
uint8_to_bcd(hour),
mode == ALARM_MODE_WEEKLY ? static_cast<uint8_t>(ALARM_DAY_OF_WEEK | day_of_week) : uint8_to_bcd(day_of_month),
};
switch (mode) {
case ALARM_MODE_EVERY_MINUTE:
regs[0] |= ALARM_MASK_BIT;
[[fallthrough]];
case ALARM_MODE_HOURLY:
regs[1] |= ALARM_MASK_BIT;
[[fallthrough]];
case ALARM_MODE_DAILY:
regs[2] |= ALARM_MASK_BIT;
break;
default: // MONTHLY / WEEKLY: everything matches
break;
}
ESP_LOGD(TAG, "Setting alarm 2: mode %u, %02u:%02u, day of month %u, day of week %u, %s", mode, hour, minute,
day_of_month, day_of_week, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
this->write_alarm_(2, regs, sizeof(regs), enabled);
}
void DS3231Component::write_alarm_(uint8_t alarm, const uint8_t *regs, size_t len, bool enabled) {
if (this->is_failed())
return;
if (!this->status_comm_result(this->write_bytes(alarm == 1 ? DS3231_REG_ALARM_1 : DS3231_REG_ALARM_2, regs, len)))
return;
// Changing the registers may have matched on the way: start from a clean flag. Enabling the
// alarm with a stale flag would assert INT/SQW right away.
if (!this->clear_alarm_flag_(alarm)) {
ESP_LOGW(TAG, "Alarm %u written, but its flag could not be cleared; not changing its enable.", alarm);
return;
}
this->set_alarm_enabled_(alarm, enabled);
}
void DS3231Component::set_alarm_enabled_(uint8_t alarm, bool enabled) {
if (alarm != 1 && alarm != 2) {
ESP_LOGE(TAG, "Invalid alarm number %u.", alarm);
return;
}
if (this->is_failed())
return;
const uint8_t bit = alarm == 1 ? CONTROL_A1IE : CONTROL_A2IE;
if (!this->update_control_(bit, enabled ? bit : 0))
return;
if (enabled && (this->control_reg_ & CONTROL_INTCN) == 0) {
ESP_LOGW(TAG, "Alarm %u enabled, but INT/SQW is a square-wave output: only the flag will be set.", alarm);
}
ESP_LOGD(TAG, "Alarm %u %s.", alarm, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
}
void DS3231Component::enable_alarm(uint8_t alarm) {
// The chip sets the flag on every match even while the alarm is disabled; enabling with it set
// would fire the trigger (and INT/SQW) for that old match right away.
if (this->clear_alarm_flag_(alarm))
this->set_alarm_enabled_(alarm, true);
}
void DS3231Component::clear_alarm(uint8_t alarm) { this->clear_alarm_flag_(alarm); }
bool DS3231Component::clear_alarm_flag_(uint8_t alarm) {
if (alarm != 1 && alarm != 2) {
ESP_LOGE(TAG, "Invalid alarm number %u.", alarm);
return false;
}
if (this->is_failed() || !this->read_control_status_())
return false;
const uint8_t flag = alarm == 1 ? STATUS_A1F : STATUS_A2F;
if ((this->status_reg_ & flag) != 0) {
// On failure the flag is still set on the chip: keep reporting the alarm as it is.
if (!this->write_status_(flag))
return false;
this->status_reg_ &= ~flag;
}
this->alarm_active_[alarm - 1] = false;
this->publish_alarm_state_(alarm, false);
return true;
}
bool DS3231Component::is_alarm_triggered(uint8_t alarm) {
if (this->is_failed() || !this->read_control_status_())
return false;
return (this->status_reg_ & (alarm == 1 ? STATUS_A1F : STATUS_A2F)) != 0;
}
void DS3231Component::check_alarms_() {
if (this->is_failed() || !this->read_control_status_())
return;
this->publish_oscillator_stopped_();
uint8_t to_clear = 0;
for (uint8_t alarm = 1; alarm <= 2; alarm++) {
const uint8_t flag = alarm == 1 ? STATUS_A1F : STATUS_A2F;
const uint8_t enable = alarm == 1 ? CONTROL_A1IE : CONTROL_A2IE;
const bool triggered = (this->status_reg_ & flag) != 0;
const bool enabled = (this->control_reg_ & enable) != 0;
bool &active = this->alarm_active_[alarm - 1];
if (!triggered) {
if (active && !this->auto_clear_alarms_)
this->publish_alarm_state_(alarm, false);
active = false;
continue;
}
// The flag is set on every match even when the alarm is disabled; only enabled alarms count.
if (enabled && !active) {
ESP_LOGI(TAG, "Alarm %u triggered.", alarm);
active = true;
this->publish_alarm_state_(alarm, true);
this->alarm_callback_.call(alarm);
}
if (this->auto_clear_alarms_)
to_clear |= flag;
}
if (to_clear != 0 && this->write_status_(to_clear)) {
this->status_reg_ &= ~to_clear;
for (uint8_t alarm = 1; alarm <= 2; alarm++) {
if ((to_clear & (alarm == 1 ? STATUS_A1F : STATUS_A2F)) == 0 || !this->alarm_active_[alarm - 1])
continue;
this->alarm_active_[alarm - 1] = false;
#ifdef USE_BINARY_SENSOR
// Keep the binary sensor on long enough for the state to be seen, then turn it off again.
this->set_timeout(ALARM_PULSE_TIMEOUT_ID + alarm - 1, ALARM_PULSE_MS,
[this, alarm]() { this->publish_alarm_state_(alarm, false); });
#endif
}
}
}
void DS3231Component::publish_alarm_state_(uint8_t alarm, bool state) {
#ifdef USE_BINARY_SENSOR
auto *sensor = alarm == 1 ? this->alarm_1_binary_sensor_ : this->alarm_2_binary_sensor_;
if (sensor != nullptr)
sensor->publish_state(state);
#endif
}
void DS3231Component::publish_oscillator_stopped_() {
#ifdef USE_BINARY_SENSOR
if (this->oscillator_stopped_binary_sensor_ != nullptr)
this->oscillator_stopped_binary_sensor_->publish_state((this->status_reg_ & STATUS_OSF) != 0);
#endif
}
// ---------------------------------------------------------------------------------------------
// Outputs and calibration
// ---------------------------------------------------------------------------------------------
void DS3231Component::enable_square_wave(SquareWaveFrequency frequency) {
if (this->interrupt_pin_ != nullptr) {
ESP_LOGE(TAG, "Can't enable the square wave: INT/SQW is used as the alarm interrupt pin.");
return;
}
if (this->is_failed())
return;
if (this->update_control_(CONTROL_INTCN | CONTROL_RS_MASK, frequency << CONTROL_RS_SHIFT)) {
ESP_LOGD(TAG, "Square wave enabled at %s.", LOG_STR_ARG(square_wave_to_string(frequency)));
}
}
void DS3231Component::disable_square_wave() {
if (this->is_failed())
return;
if (this->update_control_(CONTROL_INTCN, CONTROL_INTCN)) {
ESP_LOGD(TAG, "Square wave disabled, INT/SQW is the alarm interrupt.");
}
}
void DS3231Component::set_output_32khz(bool enabled) {
if (this->is_failed() || !this->read_control_status_())
return;
assign_bits(this->status_reg_, STATUS_EN32KHZ, enabled);
if (this->write_status_(0)) {
ESP_LOGD(TAG, "32 kHz output %s.", ONOFF(enabled));
}
}
void DS3231Component::set_aging_offset(int8_t offset) {
if (this->is_failed())
return;
if (!this->write_aging_offset_(offset))
return;
ESP_LOGD(TAG, "Aging offset set to %d.", offset);
// The new offset takes effect with the next temperature conversion.
this->force_temperature_conversion();
}
// ---------------------------------------------------------------------------------------------
// Register access
// ---------------------------------------------------------------------------------------------
bool DS3231Component::read_control_status_() {
uint8_t raw[2];
if (!this->read_bytes(DS3231_REG_CONTROL, raw, sizeof(raw))) {
ESP_LOGW(TAG, "Can't read control/status registers.");
if (!this->status_comm_result(this->read_bytes(DS3231_REG_CONTROL, raw, sizeof(raw))))
return false;
}
this->control_reg_ = raw[0];
this->control_reg_ = raw[0] & ~CONTROL_CONV;
this->status_reg_ = raw[1];
return true;
}
bool DS3231Component::write_control_() {
if (!this->write_byte(DS3231_REG_CONTROL, this->control_reg_)) {
ESP_LOGW(TAG, "Can't write control register.");
bool DS3231Component::update_control_(uint8_t mask, uint8_t value) {
// CONV is a one-shot command bit: always write it back as 0 so a control change never starts
// a temperature conversion.
if (!this->status_comm_result(this->update_register(DS3231_REG_CONTROL, mask | CONTROL_CONV, value)))
return false;
}
this->control_reg_ = (this->control_reg_ & ~mask) | (value & mask);
return true;
}
bool DS3231Component::write_status_(uint8_t clear) {
// The flags can only be written to 0; writing 1 leaves them unchanged, so a flag that was set
// between the read and this write is never lost.
const uint8_t value = ((this->status_reg_ & STATUS_EN32KHZ) | STATUS_FLAGS) & ~clear;
return this->status_comm_result(this->write_byte(DS3231_REG_STATUS, value));
}
bool DS3231Component::write_aging_offset_(int8_t offset) {
if (!this->status_comm_result(this->write_byte(DS3231_REG_AGING_OFFSET, static_cast<uint8_t>(offset))))
return false;
this->aging_offset_ = offset;
return true;
}
+132 -1
View File
@@ -1,32 +1,163 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/i2c/i2c.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
namespace esphome::ds3231 {
/// When an alarm fires. Alarm 1 supports every mode, alarm 2 has no seconds register and so no EVERY_SECOND.
enum AlarmMode : uint8_t {
ALARM_MODE_EVERY_SECOND = 0, ///< Alarm 1 only.
ALARM_MODE_EVERY_MINUTE, ///< Alarm 1: when the seconds match. Alarm 2: at second 00 of every minute.
ALARM_MODE_HOURLY, ///< When minutes (and seconds) match.
ALARM_MODE_DAILY, ///< When hours, minutes (and seconds) match.
ALARM_MODE_MONTHLY, ///< When date, hours, minutes (and seconds) match.
ALARM_MODE_WEEKLY, ///< When day of week, hours, minutes (and seconds) match.
};
/// Frequency of the INT/SQW pin when it is a square-wave output (RS2:RS1 in the control register).
enum SquareWaveFrequency : uint8_t {
SQUARE_WAVE_1HZ = 0,
SQUARE_WAVE_1024HZ = 1,
SQUARE_WAVE_4096HZ = 2,
SQUARE_WAVE_8192HZ = 3,
};
/// Hub for a single DS3231 real-time clock. Owns all register access; the platforms talk to the
/// chip through this class.
class DS3231Component final : public Component, public i2c::I2CDevice {
public:
/// Seconds through year, the 7-byte BCD calendar the time platform decodes with time::DS_BCD_CALENDAR.
static constexpr size_t CALENDAR_LEN = 7;
void setup() override;
void loop() override;
void dump_config() override;
// ---- Configuration (set from codegen) ----
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_square_wave_frequency(SquareWaveFrequency frequency) {
this->square_wave_frequency_ = frequency;
this->square_wave_configured_ = true;
}
void set_battery_backed_square_wave(bool enabled) { this->battery_backed_square_wave_ = enabled ? 1 : 0; }
void set_output_32khz_config(bool enabled) { this->output_32khz_ = enabled ? 1 : 0; }
void set_aging_offset_config(int8_t offset) {
this->aging_offset_ = offset;
this->aging_offset_configured_ = true;
}
void set_auto_clear_alarms(bool auto_clear) { this->auto_clear_alarms_ = auto_clear; }
void set_alarm_check_interval(uint32_t interval) { this->alarm_check_interval_ = interval; }
void set_monitor_alarms(bool monitor) { this->monitor_alarms_ = monitor; }
/// Register a void(uint8_t alarm) callback that runs when an enabled alarm (1 or 2) matches.
template<typename F> void add_on_alarm_callback(F &&callback) {
this->alarm_callback_.add(std::forward<F>(callback));
}
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(alarm_1)
SUB_BINARY_SENSOR(alarm_2)
SUB_BINARY_SENSOR(oscillator_stopped)
#endif
// ---- Clock registers (used by the time platform) ----
/// Read the calendar registers into @p raw. Fails if the oscillator was stopped since the time was last set.
bool read_calendar(uint8_t *raw);
/// Write the calendar registers from @p raw and clear the oscillator-stop flag.
bool write_calendar(const uint8_t *raw);
// ---- Temperature ----
bool read_temperature(float &out);
void force_temperature_conversion();
// ---- Alarms ----
/// Program alarm 1. @p day_of_month is used in MONTHLY mode, @p day_of_week (1 = Sunday) in WEEKLY mode.
void set_alarm_1(AlarmMode mode, uint8_t second, uint8_t minute, uint8_t hour, uint8_t day_of_month,
uint8_t day_of_week, bool enabled);
/// Program alarm 2 (minute resolution, fires at second 00).
void set_alarm_2(AlarmMode mode, uint8_t minute, uint8_t hour, uint8_t day_of_month, uint8_t day_of_week,
bool enabled);
/// Enable @p alarm. A match from while it was disabled is cleared first, so only the next one fires.
void enable_alarm(uint8_t alarm);
void disable_alarm(uint8_t alarm) { this->set_alarm_enabled_(alarm, false); }
/// Clear the alarm flag, which also releases the INT/SQW line.
void clear_alarm(uint8_t alarm);
/// True while the alarm flag of @p alarm (1 or 2) is set on the chip.
bool is_alarm_triggered(uint8_t alarm);
// ---- Outputs and calibration ----
/// Switch INT/SQW to a square-wave output at @p frequency. Alarms then no longer drive the pin.
void enable_square_wave(SquareWaveFrequency frequency);
/// Switch INT/SQW back to the (active-low) alarm interrupt output.
void disable_square_wave();
void set_output_32khz(bool enabled);
/// Aging offset in LSB (about 0.1 ppm each at 25 degC); positive values slow the clock down.
void set_aging_offset(int8_t offset);
protected:
static constexpr uint8_t CONTROL_A1IE = 0x01;
static constexpr uint8_t CONTROL_A2IE = 0x02;
static constexpr uint8_t CONTROL_INTCN = 0x04;
static constexpr uint8_t CONTROL_RS_MASK = 0x18;
static constexpr uint8_t CONTROL_RS_SHIFT = 3;
static constexpr uint8_t CONTROL_CONV = 0x20;
static constexpr uint8_t CONTROL_BBSQW = 0x40;
static constexpr uint8_t CONTROL_EOSC = 0x80;
static constexpr uint8_t STATUS_A1F = 0x01;
static constexpr uint8_t STATUS_A2F = 0x02;
static constexpr uint8_t STATUS_BSY = 0x04;
static constexpr uint8_t STATUS_EN32KHZ = 0x08;
static constexpr uint8_t STATUS_OSF = 0x80;
static constexpr uint8_t STATUS_FLAGS = STATUS_A1F | STATUS_A2F | STATUS_OSF;
/// Scheduler ids: one pulse timer per alarm (id + alarm - 1).
static constexpr uint32_t ALARM_PULSE_TIMEOUT_ID = 0;
static void interrupt_handler(DS3231Component *arg);
/// Read the control and status registers into the cached copies. Sets the warning flag on failure.
bool read_control_status_();
bool write_control_();
/// Replace the bits in @p mask of the control register with @p value, on the chip and in the cache.
bool update_control_(uint8_t mask, uint8_t value);
/// Write the status register, clearing the flags in @p clear (writing 1 to a flag leaves it unchanged).
bool write_status_(uint8_t clear);
bool write_aging_offset_(int8_t offset);
/// Clear the flag of @p alarm and report it off. Returns false (state unchanged) if that fails.
bool clear_alarm_flag_(uint8_t alarm);
void set_alarm_enabled_(uint8_t alarm, bool enabled);
/// Write the alarm registers, clear the stale flag and set the interrupt enable.
void write_alarm_(uint8_t alarm, const uint8_t *regs, size_t len, bool enabled);
/// Read the flags, fire triggers and update the binary sensors.
void check_alarms_();
void publish_alarm_state_(uint8_t alarm, bool state);
void publish_oscillator_stopped_();
InternalGPIOPin *interrupt_pin_{nullptr};
LazyCallbackManager<void(uint8_t)> alarm_callback_;
uint32_t alarm_check_interval_{1000};
uint8_t control_reg_{0};
uint8_t status_reg_{0};
int8_t aging_offset_{0};
SquareWaveFrequency square_wave_frequency_{SQUARE_WAVE_1HZ};
/// -1: leave the chip as it is, 0: off, 1: on.
int8_t battery_backed_square_wave_{-1};
int8_t output_32khz_{-1};
bool square_wave_configured_{false};
bool aging_offset_configured_{false};
bool auto_clear_alarms_{true};
bool monitor_alarms_{false};
/// Set while an alarm flag has been reported, so a latched flag fires its trigger only once.
bool alarm_active_[2]{false, false};
};
} // namespace esphome::ds3231
@@ -0,0 +1,40 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import time
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import CONF_DS3231_ID, DS3231Component, ds3231_ns
DEPENDENCIES = ["ds3231"]
DS3231Time = ds3231_ns.class_(
"DS3231Time", time.RealTimeClock, cg.Parented.template(DS3231Component)
)
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DS3231Time),
cv.GenerateID(CONF_DS3231_ID): cv.use_id(DS3231Component),
}
)
DS3231_TIME_ACTION_SCHEMA = automation.maybe_simple_id(
{cv.GenerateID(): cv.use_id(DS3231Time)}
)
automation.register_apply_action(
"ds3231.write_time", DS3231_TIME_ACTION_SCHEMA, automation.ApplyCall("write_time()")
)
automation.register_apply_action(
"ds3231.read_time", DS3231_TIME_ACTION_SCHEMA, automation.ApplyCall("read_time()")
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_DS3231_ID])
await time.register_time(var, config)
@@ -0,0 +1,48 @@
#include "ds3231_time.h"
#include "esphome/core/log.h"
namespace esphome::ds3231 {
ESPHOME_LOG_TAG(TAG, "ds3231.time");
void DS3231Time::update() { this->read_time(); }
void DS3231Time::dump_config() {
ESP_LOGCONFIG(TAG, "DS3231 Time:");
RealTimeClock::dump_config();
}
void DS3231Time::read_time() {
uint8_t raw[DS3231Component::CALENDAR_LEN];
if (!this->status_comm_result(this->parent_->read_calendar(raw), LOG_STR("No valid time from the RTC")))
return;
const ESPTime rtc_time = time::decode_bcd_calendar<time::DS_BCD_CALENDAR>(raw);
ESP_LOGD(TAG, "Read UTC time: %04u-%02u-%02u %02u:%02u:%02u (day of week %u)", rtc_time.year, rtc_time.month,
rtc_time.day_of_month, rtc_time.hour, rtc_time.minute, rtc_time.second, rtc_time.day_of_week);
// The day of week is a free-running counter on the DS3231 and may have been set with a different
// numbering by other software, so it is not used to validate the time.
if (!this->status_comm_result(rtc_time.is_valid(/*check_day_of_week=*/false, /*check_day_of_year=*/false),
LOG_STR("Invalid RTC time")))
return;
this->synchronize_epoch_(rtc_time.timestamp);
}
void DS3231Time::write_time() {
auto now = this->utcnow();
if (!now.is_valid()) {
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
return;
}
if (now.year > 2199) {
ESP_LOGE(TAG, "Year %u is out of the DS3231 range (2000-2199).", now.year);
return;
}
uint8_t raw[DS3231Component::CALENDAR_LEN];
time::encode_bcd_calendar<time::DS_BCD_CALENDAR>(now, raw);
if (!this->status_comm_result(this->parent_->write_calendar(raw)))
return;
ESP_LOGD(TAG, "Wrote UTC time: %04u-%02u-%02u %02u:%02u:%02u", now.year, now.month, now.day_of_month, now.hour,
now.minute, now.second);
}
} // namespace esphome::ds3231
@@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/time/real_time_clock.h"
#include "../ds3231.h"
namespace esphome::ds3231 {
class DS3231Time final : public time::RealTimeClock, public Parented<DS3231Component> {
public:
void update() override;
void dump_config() override;
/// Set the system clock from the RTC.
void read_time();
/// Store the system clock in the RTC.
void write_time();
};
} // namespace esphome::ds3231
+16
View File
@@ -2213,6 +2213,22 @@ def idf_version() -> cv.Version:
return CORE.data[KEY_ESP32][KEY_IDF_VERSION]
def require_idf_version(
minimum: cv.Version, feature: str = "This feature"
) -> Callable[[Any], Any]:
"""Validator for an option that needs the underlying ESP-IDF at ``minimum`` or newer."""
def validator(value: Any) -> Any:
cv.only_on_esp32(value)
if (version := idf_version()) < minimum:
raise cv.Invalid(
f"{feature} requires ESP-IDF {minimum} or newer, got {version}"
)
return value
return validator
def require_fatfs() -> None:
"""Mark that FATFS support is required by a component.
@@ -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 -1
View File
@@ -37,6 +37,7 @@ from esphome.const import (
)
from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority
from esphome.core.entity_helpers import (
RESTORE_STATE_NONE,
entity_duplicate_validator,
queue_entity_register,
setup_entity,
@@ -56,7 +57,6 @@ from .effects import (
)
from .restore_state import (
LEGACY_RESTORE_MODES,
RESTORE_STATE_NONE,
RESTORE_STATE_SCHEMA,
_build_state_lambda,
_initial_state_overridden_by_legacy_mode,
+24 -30
View File
@@ -11,6 +11,14 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_COLOR_MODE, CONF_STATE
from esphome.core import Lambda
from esphome.core.entity_helpers import (
RESTORE_STATE_INITIAL,
RESTORE_STATE_INVERT,
RESTORE_STATE_KEEP,
RESTORE_STATE_NONE,
restore_state_keyword,
restore_state_schema,
)
from esphome.cpp_generator import call_lambda
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
@@ -18,12 +26,6 @@ from esphome.types import ConfigType
from .automation import LIGHT_STATE_FIELDS, validate_light_state
from .types import ColorMode, LightStateRTCState
RESTORE_STATE_KEEP = "KEEP"
RESTORE_STATE_INVERT = "INVERT"
RESTORE_STATE_INITIAL = "INITIAL"
RESTORE_STATE_ALL = "ALL"
RESTORE_STATE_NONE = "NONE"
@dataclass(frozen=True)
class LegacyRestoreMode:
@@ -290,10 +292,12 @@ def _keep_or(validator: Callable[[Any], Any]) -> Callable[[Any], Any]:
except cv.Invalid:
inner = ()
return (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL, *inner)
if isinstance(value, str):
upper = value.strip().upper()
if upper in (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL):
return upper
if (
keyword := restore_state_keyword(
value, (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL)
)
) is not None:
return keyword
return validator(value)
return validate
@@ -308,10 +312,12 @@ def _validate_restore_state_state(value: Any) -> str | bool:
RESTORE_STATE_INITIAL,
*validate_light_state(SCHEMA_EXTRACT),
)
if isinstance(value, str):
upper = value.strip().upper()
if upper in (RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL):
return upper
if (
keyword := restore_state_keyword(
value, (RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL)
)
) is not None:
return keyword
return validate_light_state(value)
@@ -327,19 +333,7 @@ _RESTORE_STATE_FIELDS_SCHEMA = cv.Schema(
)
@schema_extractor("schema")
def RESTORE_STATE_SCHEMA(value: Any) -> ConfigType | str:
"""
The restore_state: config key can be a mapping of per-field overrides, `all`, or `none`.
"""
if value == SCHEMA_EXTRACT:
# The `all`/`none` string shorthands have no representation in the extracted
# docs schema; only the per-field mapping form is walked here.
return _RESTORE_STATE_FIELDS_SCHEMA
if isinstance(value, str):
upper = value.strip().upper()
if upper == RESTORE_STATE_ALL:
value = {}
elif upper == RESTORE_STATE_NONE:
return RESTORE_STATE_NONE
return _RESTORE_STATE_FIELDS_SCHEMA(value)
# A mapping of per-field overrides, `all`, or `none`, which stays the keyword for to_code
RESTORE_STATE_SCHEMA = restore_state_schema(
_RESTORE_STATE_FIELDS_SCHEMA, RESTORE_STATE_NONE
)
+14 -10
View File
@@ -35,12 +35,12 @@ class LockTraits {
public:
LockTraits() = default;
bool get_supports_open() const { return this->supports_open_; }
void set_supports_open(bool supports_open) { this->supports_open_ = supports_open; }
bool get_requires_code() const { return this->requires_code_; }
void set_requires_code(bool requires_code) { this->requires_code_ = requires_code; }
bool get_assumed_state() const { return this->assumed_state_; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
bool get_supports_open() const { return this->flags_.supports_open; }
void set_supports_open(bool supports_open) { this->flags_.supports_open = supports_open; }
bool get_requires_code() const { return this->flags_.requires_code; }
void set_requires_code(bool requires_code) { this->flags_.requires_code = requires_code; }
bool get_assumed_state() const { return this->flags_.assumed_state; }
void set_assumed_state(bool assumed_state) { this->flags_.assumed_state = assumed_state; }
bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); }
void set_supported_states(std::initializer_list<LockState> states) {
@@ -54,9 +54,13 @@ class LockTraits {
void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); }
protected:
bool supports_open_{false};
bool requires_code_{false};
bool assumed_state_{false};
// Bit-packed flags to save memory (1 byte instead of 3)
struct LockTraitFlags {
uint8_t supports_open : 1;
uint8_t requires_code : 1;
uint8_t assumed_state : 1;
uint8_t reserved : 5;
} flags_{};
uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)};
};
@@ -176,8 +180,8 @@ class Lock : public EntityBase {
*/
virtual void control(const LockCall &call) = 0;
Deduplicator<LockState> publish_dedup_; // packs with state and traits ahead of the pointer members
LazyCallbackManager<void(LockState)> state_callback_{};
Deduplicator<LockState> publish_dedup_;
ESPPreferenceObject rtc_;
};
+5 -2
View File
@@ -1,5 +1,4 @@
#include "mcp23016.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstdio>
@@ -80,7 +79,11 @@ void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg_(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << pin, pin_value);
if (pin_value) {
reg_value |= 1 << pin;
} else {
reg_value &= ~(1 << pin);
}
this->write_reg_(reg_addr, reg_value);
@@ -1,5 +1,4 @@
#include "mcp23x08_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::mcp23x08_base {
@@ -79,7 +78,11 @@ void MCP23X08Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << bit, pin_value);
if (pin_value) {
reg_value |= 1 << bit;
} else {
reg_value &= ~(1 << bit);
}
this->write_reg(reg_addr, reg_value);
@@ -92,7 +92,11 @@ void MCP23X17Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << bit, pin_value);
if (pin_value) {
reg_value |= 1 << bit;
} else {
reg_value &= ~(1 << bit);
}
this->write_reg(reg_addr, reg_value);
+4
View File
@@ -50,6 +50,10 @@ void MQTTCoverComponent::setup() {
}
if (traits.get_supports_tilt()) {
this->subscribe(this->get_tilt_command_topic(), [this](const std::string &topic, const std::string &payload) {
if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("STOP")) == 0) {
this->cover_->make_call().set_command_stop_tilt().perform();
return;
}
auto value = parse_number<float>(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str());
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca6416a.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca6416a {
@@ -171,7 +170,11 @@ void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t re
this->read_register_(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << bit, pin_value);
if (pin_value) {
reg_value |= 1 << bit;
} else {
reg_value &= ~(1 << bit);
}
this->write_register_(reg_addr, reg_value);
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca9505.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca9505 {
@@ -75,7 +74,11 @@ bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
assign_bits(this->output_[bank], mask, value);
if (value) {
this->output_[bank] |= mask;
} else {
this->output_[bank] &= ~mask;
}
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
}
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca9554.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca9554 {
@@ -100,7 +99,11 @@ bool PCA9554Component::digital_read_cache(uint8_t pin) {
}
void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
#ifdef USE_PCA9554_PCAL
if (this->write_register_(OUTPUT_REG, this->output_mask_))
this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
+5 -1
View File
@@ -60,7 +60,11 @@ bool PCF8574Component::digital_read_hw(uint8_t pin) {
bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void PCF8574Component::digital_write_hw(uint8_t pin, bool value) {
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
this->write_gpio_();
}
void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
@@ -1,5 +1,4 @@
#include "pi4ioe5v6408.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pi4ioe5v6408 {
@@ -130,7 +129,11 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
if (!this->status_comm_result(this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_),
LOG_STR("Failed to write output register")))
return;
+6 -16
View File
@@ -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
+11 -1
View File
@@ -92,7 +92,17 @@ class JPEGFormat(Format):
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
# 1.8.4 plus decodeBegin()/decodeRows(), so an image can be decoded a few rows at a time and
# can wait for input that has not arrived yet, and fixes for: input that arrives in short
# reads, garbled RGB8888 output of grayscale images, and two ARM NEON faults seen on arm64 hosts
# (swapped red and blue in the 4:2:0 RGB8888 path, writes past the row on clipped MCUs). It also
# uses the ESP32-S3 SIMD colour conversion in ESP-IDF builds, not only Arduino ones, and is built
# with -O2.
cg.add_library(
"JPEGDEC",
"1.8.4",
"https://github.com/esphome-libs/JPEGDEC#1.8.4-esphome.3",
)
if CORE.is_host:
# JPEGDEC's host detection checks __MACH__/__LINUX__, but gcc only
# predefines the lowercase __linux__; without this a Linux host
@@ -1,4 +1,6 @@
#pragma once
#include <cstdint>
#include "esphome/core/color.h"
#include "image_format.h"
@@ -9,6 +11,14 @@ enum DecodeError : int {
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
DECODE_ERROR_OUT_OF_MEMORY = -3,
DECODE_ERROR_INTERNAL_DECODER_ERROR = -4,
DECODE_ERROR_READ = -5,
};
/// Results of ImageDecoder::decode_step() other than a DecodeError
enum DecodeStep : int {
DECODE_STEP_DONE = 0,
DECODE_STEP_MORE = 1,
DECODE_STEP_NEED_DATA = 2, // The source has nothing more yet; call again once more has arrived
};
constexpr const char *decode_error_to_string(int error) {
@@ -21,6 +31,8 @@ constexpr const char *decode_error_to_string(int error) {
return "Out of memory";
case DECODE_ERROR_INTERNAL_DECODER_ERROR:
return "Internal decoder error";
case DECODE_ERROR_READ:
return "Read error";
default:
return "Unknown error";
}
@@ -28,6 +40,19 @@ constexpr const char *decode_error_to_string(int error) {
class RuntimeImage;
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
class DecodeSource {
public:
virtual ~DecodeSource() = default;
/**
* Read up to len bytes at the current position. Returns the number read, which is less than len, even 0, when
* no more has arrived yet or at the end of the input, or negative on error.
*/
virtual int32_t read(uint8_t *buf, uint32_t len) = 0;
/// Move to a position counted from the start. Returns false on error.
virtual bool seek(uint32_t position) = 0;
};
/**
* @brief Class to abstract decoding different image formats.
*/
@@ -88,6 +113,28 @@ class ImageDecoder {
*/
virtual int decode(uint8_t *buffer, size_t size) = 0;
/// Whether this decoder can read a DecodeSource itself, a step at a time, instead of being fed data.
virtual bool supports_source() const { return false; }
/**
* @brief Start decoding from a source the decoder reads itself. Call after prepare(), in place of decode().
*
* The image headers (everything before the image data) must be readable from the source when this is called;
* only decode_step() can wait for more to arrive. The source must hold expected_size bytes in the end, and stay
* valid until the session ends.
* @return int 0 on success, or a {@see DecodeError} value.
*/
virtual int begin_source(DecodeSource *source) { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
/**
* @brief Decode a little more of an image started with begin_source(), keeping each call short.
*
* @return int DECODE_STEP_MORE while there is more to do, DECODE_STEP_NEED_DATA when the source has not
* delivered enough to go on, DECODE_STEP_DONE when the image is complete, or a {@see DecodeError}
* value.
*/
virtual int decode_step() { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
/**
* @brief Request the image to be resized once the actual dimensions are known.
* Called by the callback functions, to be able to access the parent Image class.
@@ -112,6 +159,9 @@ class ImageDecoder {
*/
void draw(int x, int y, int w, int h, const Color &color);
/// Bytes of the image the decoder has used so far
size_t get_decoded_bytes() const { return this->decoded_bytes_; }
/**
* @brief Check if the decoder has finished processing.
*
+114 -20
View File
@@ -135,13 +135,24 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
return DECODE_ERROR_INVALID_TYPE;
}
const int err = this->start_();
if (err != 0)
return err;
if (!this->jpeg_.decode(0, 0, 0))
return this->fail_();
this->decoded_bytes_ = size;
this->jpeg_.close();
return size;
}
int JpegDecoder::start_() {
auto jpeg_type = this->jpeg_.getJPEGType();
if (jpeg_type == JPEG_MODE_INVALID) {
ESP_LOGE(TAG, "Unsupported JPEG image");
return DECODE_ERROR_INVALID_TYPE;
} else if (jpeg_type == JPEG_MODE_PROGRESSIVE) {
ESP_LOGE(TAG, "Progressive JPEG images not supported");
return DECODE_ERROR_INVALID_TYPE;
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
@@ -160,26 +171,109 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
if (!this->jpeg_.decode(0, 0, 0)) {
auto error = this->jpeg_.getLastError();
ESP_LOGE(TAG, "Error while decoding: %d", error);
this->jpeg_.close();
switch (error) {
case JPEG_ERROR_MEMORY:
return DECODE_ERROR_OUT_OF_MEMORY;
case JPEG_UNSUPPORTED_FEATURE:
return DECODE_ERROR_UNSUPPORTED_FORMAT;
case JPEG_INVALID_FILE:
case JPEG_INVALID_PARAMETER:
return DECODE_ERROR_INVALID_TYPE;
case JPEG_DECODE_ERROR:
default:
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
}
}
this->decoded_bytes_ = size;
return 0;
}
int JpegDecoder::fail_() {
const bool read_failed = this->source_ != nullptr && this->source_failed_;
const int error = this->jpeg_.getLastError();
this->jpeg_.close();
return size;
this->source_ = nullptr;
if (read_failed) {
ESP_LOGE(TAG, "Reading the image failed");
return DECODE_ERROR_READ;
}
ESP_LOGE(TAG, "Error while decoding: %d", error);
switch (error) {
case JPEG_ERROR_MEMORY:
return DECODE_ERROR_OUT_OF_MEMORY;
case JPEG_UNSUPPORTED_FEATURE:
return DECODE_ERROR_UNSUPPORTED_FORMAT;
case JPEG_INVALID_FILE:
case JPEG_INVALID_PARAMETER:
return DECODE_ERROR_INVALID_TYPE;
case JPEG_DECODE_ERROR:
default:
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
}
}
void JpegDecoder::reset() {
// A decode from a source that was given up part way still has the image open
if (this->source_ != nullptr) {
this->jpeg_.close();
this->source_ = nullptr;
}
ImageDecoder::reset();
}
int32_t JpegDecoder::source_read(JPEGFILE *file, uint8_t *buf, int32_t len) {
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
const int32_t n = decoder->source_->read(buf, len);
if (n < 0) {
// JPEGDEC has no way to report a failed read, so remember it and give it no data
decoder->source_failed_ = true;
return 0;
}
file->iPos += n;
return n;
}
int32_t JpegDecoder::source_seek(JPEGFILE *file, int32_t position) {
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
if (position < 0 || !decoder->source_->seek(position)) {
decoder->source_failed_ = true;
return file->iPos;
}
file->iPos = position;
return position;
}
int JpegDecoder::begin_source(DecodeSource *source) {
if (this->expected_size_ == 0 || this->expected_size_ > INT32_MAX) {
ESP_LOGE(TAG, "Image size %zu cannot be read from a source", this->expected_size_);
return DECODE_ERROR_INVALID_TYPE;
}
if (!source->seek(0)) {
ESP_LOGE(TAG, "Could not read the image");
return DECODE_ERROR_READ;
}
this->source_ = source;
this->source_failed_ = false;
if (!this->jpeg_.open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
// The caller may recover, for example by reading the whole image first, so it reports the failure
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
this->source_ = nullptr;
return this->source_failed_ ? DECODE_ERROR_READ : DECODE_ERROR_INVALID_TYPE;
}
const int err = this->start_();
if (err != 0) {
this->jpeg_.close();
this->source_ = nullptr;
return err;
}
if (!this->jpeg_.decodeBegin(0, 0, 0))
return this->fail_();
return 0;
}
int HOT JpegDecoder::decode_step() {
if (this->source_ == nullptr)
return DECODE_ERROR_INVALID_TYPE;
const int result = this->jpeg_.decodeRows(1);
// JPEGDEC may decode on with the missing data, so check the read before its result
if (this->source_failed_)
return this->fail_();
if (result == JPEG_DECODE_MORE)
return DECODE_STEP_MORE;
if (result == JPEG_DECODE_NEED_DATA)
return DECODE_STEP_NEED_DATA;
if (result == 0)
return this->fail_();
this->decoded_bytes_ = this->expected_size_;
this->jpeg_.close();
this->source_ = nullptr;
return DECODE_STEP_DONE;
}
} // namespace esphome::runtime_image
@@ -22,19 +22,36 @@ class JpegDecoder : public ImageDecoder {
~JpegDecoder() override {}
int HOT decode(uint8_t *buffer, size_t size) override;
void reset() override;
/// The source is read as the decode needs it, through JPEGDEC's own 2 KB buffer
bool supports_source() const override { return true; }
int begin_source(DecodeSource *source) override;
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
int HOT decode_step() override;
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
protected:
bool prepare_rgb565_scaling_(int width, int height);
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
int start_();
/// The DecodeError for a failed read of the source or JPEGDEC's last error, closing the image
int fail_();
static int32_t source_read(JPEGFILE *file, uint8_t *buf, int32_t len);
static int32_t source_seek(JPEGFILE *file, int32_t position);
static void source_close(void *handle) {}
JPEGDEC jpeg_{};
FixedVector<uint16_t> x_boundaries_{};
FixedVector<uint16_t> y_boundaries_{};
int source_width_{0};
int source_height_{0};
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
bool direct_rgb565_{false};
bool source_failed_{false};
};
} // namespace esphome::runtime_image
@@ -239,6 +239,25 @@ int RuntimeImage::feed_data(uint8_t *data, size_t len) {
return consumed;
}
int RuntimeImage::begin_source(DecodeSource *source) {
if (!this->decoder_supports_source()) {
ESP_LOGE(TAG, "Decoder cannot read a source");
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
return this->decoder_->begin_source(source);
}
int RuntimeImage::decode_step() {
if (!this->is_decoding()) {
ESP_LOGE(TAG, "No decoder initialized");
return DECODE_ERROR_INVALID_TYPE;
}
const int result = this->decoder_->decode_step();
if (result == DECODE_STEP_DONE)
this->decoded_bytes_ = this->decoder_->get_decoded_bytes();
return result;
}
bool RuntimeImage::end_decode() {
if (!this->is_decoding()) {
return false;
@@ -76,6 +76,22 @@ class RuntimeImage : public image::Image {
*/
int feed_data(uint8_t *data, size_t len);
/// Whether the active decoder can read a DecodeSource itself (see begin_source()).
bool decoder_supports_source() const { return this->is_decoding() && this->decoder_->supports_source(); }
/**
* @brief Decode from a source the decoder reads itself, in place of feed_data().
*
* Call after begin_decode() when decoder_supports_source() is true, once the image headers can be read from the
* source, then call decode_step() until it
* returns DECODE_STEP_DONE, and finish with end_decode().
* @return 0 on success, or a DecodeError value.
*/
int begin_source(DecodeSource *source);
/// Decode a little more from the source; see ImageDecoder::decode_step().
int decode_step();
/**
* @brief Complete the decoding process.
*
+394 -39
View File
@@ -1,79 +1,226 @@
#include "rx8111.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::rx8111 {
ESPHOME_LOG_TAG(TAG, "rx8111");
// Indexed by the TSEL value stored in timer_clock_
PROGMEM_STRING_TABLE(TimerClockNames, "4096 Hz", "64 Hz", "1 Hz", "1/60 Hz");
static constexpr uint8_t RX8111_REG_SEC = 0x10;
static constexpr uint8_t RX8111_REG_MIN_ALARM = 0x17;
static constexpr uint8_t RX8111_REG_HOUR_ALARM = 0x18;
static constexpr uint8_t RX8111_REG_DAY_ALARM = 0x19;
static constexpr uint8_t RX8111_REG_EXTENSION = 0x1D;
static constexpr uint8_t RX8111_REG_FLAG = 0x1E;
static constexpr uint8_t RX8111_REG_TIMER0 = 0x1A;
static constexpr uint8_t RX8111_REG_CONTROL = 0x1F;
static constexpr uint8_t RX8111_REG_SEC_ALARM = 0x2C;
static constexpr uint8_t RX8111_ALARM_DISABLED = 0x80;
static constexpr uint8_t RX8111_EXTENSION_WADA = 0x08;
static constexpr uint8_t RX8111_CONTROL_AIE = 0x08;
static constexpr uint8_t RX8111_CONTROL_TIE = 0x10;
static constexpr uint8_t RX8111_EXTENSION_TE = 0x10;
static constexpr uint8_t RX8111_EXTENSION_TSEL = 0x03;
static constexpr uint8_t RX8111_FLAG_TF = 0x10;
static constexpr uint8_t RX8111_FLAG_AF = 0x08;
static constexpr uint8_t RX8111_FLAG_POR = 0x80;
static constexpr uint8_t RX8111_FLAG_VLF = 0x02;
static constexpr uint8_t RX8111_FLAG_XST = 0x01;
static constexpr uint8_t RX8111_CTRL_STOP = 0x01;
// FLAG bits are write-zero-to-clear; keep UF/TF/AF/EVF even if they latch during the write.
static constexpr uint8_t RX8111_KEEP_EVENT_FLAGS = 0x3C;
static constexpr uint32_t RX8111_EVENT_INTERVAL_ID = 0;
static constexpr uint32_t RX8111_BOOT_ALARM_RETRY_ID = 1;
static constexpr uint32_t RX8111_BOOT_TIMER_RETRY_ID = 2;
static constexpr uint8_t RX8111_BOOT_MAX_ATTEMPTS = 3;
void RX8111Component::setup() { this->read_time(); }
void RX8111Component::setup() {
this->configure_boot_alarm_();
this->configure_boot_timer_();
this->read_time();
if (this->alarm_enabled_ || this->watching_()) {
this->start_event_polling_();
}
}
void RX8111Component::update() { this->read_time(); }
void RX8111Component::configure_boot_alarm_() {
bool success = true;
if (this->alarm_enabled_) {
if (this->alarm_second_ != ALARM_FIELD_UNSET || this->alarm_minute_ != ALARM_FIELD_UNSET ||
this->alarm_hour_ != ALARM_FIELD_UNSET || this->alarm_weekday_ != ALARM_FIELD_UNSET ||
this->alarm_day_ != ALARM_FIELD_UNSET) {
success = this->program_alarm_(this->alarm_second_, this->alarm_minute_, this->alarm_hour_, this->alarm_weekday_,
this->alarm_day_, true);
if (success) {
// Only the first successful boot program can introduce a boot wake-up edge.
if (!this->boot_alarm_event_seen_)
this->alarm_flag_seen_ = false;
this->start_event_polling_();
}
} else {
// Preserve an alarm programmed by an action before a reboot or deep sleep.
uint8_t control;
success = this->read_byte(RX8111_REG_CONTROL, &control);
if (success) {
this->alarm_armed_ = (control & RX8111_CONTROL_AIE) != 0;
if (this->alarm_armed_)
this->start_event_polling_();
}
}
} else if (this->alarm_explicitly_configured_) {
success = this->set_alarm_interrupt_(false);
}
this->boot_config_result_(true, success);
}
void RX8111Component::configure_boot_timer_() {
bool success = true;
// An omitted timer setting leaves battery-backed state alone; explicit false stops it.
if (this->timer_configured_) {
const bool start_timer = this->timer_enabled_;
if (start_timer) {
success = this->configure_timer_();
} else {
uint8_t extension, control;
success = this->disable_timer_(extension, control);
}
if (success && !start_timer && this->timer_count_ != 0) {
this->timer_retained_event_pending_ = true;
this->start_event_polling_();
}
}
this->boot_config_result_(false, success);
}
void RX8111Component::boot_config_result_(bool alarm, bool success) {
auto &attempts = alarm ? this->boot_alarm_attempts_ : this->boot_timer_attempts_;
const uint8_t warning = alarm ? WARN_ALARM : WARN_TIMER;
++attempts;
if (success) {
this->clear_warning_(warning);
return;
}
this->set_warning_(warning, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (attempts >= RX8111_BOOT_MAX_ATTEMPTS)
return;
this->set_timeout(alarm ? RX8111_BOOT_ALARM_RETRY_ID : RX8111_BOOT_TIMER_RETRY_ID, this->boot_retry_delay_ms_,
[this, alarm]() {
if (alarm) {
this->configure_boot_alarm_();
} else {
this->configure_boot_timer_();
}
});
}
// A runtime action supersedes pending boot programming of the same feature
void RX8111Component::cancel_boot_retry_(bool alarm) {
this->cancel_timeout(alarm ? RX8111_BOOT_ALARM_RETRY_ID : RX8111_BOOT_TIMER_RETRY_ID);
}
void RX8111Component::check_event_flags_() {
if (this->watching_() && !this->poll_event_flags_())
this->set_warning_(WARN_EVENT_POLL, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
// A callback or a runtime action may have stopped the last watched feature
if (!this->watching_() && this->event_polling_started_) {
this->cancel_interval(RX8111_EVENT_INTERVAL_ID);
this->event_polling_started_ = false;
}
}
void RX8111Component::start_event_polling_() {
if (this->event_polling_started_)
return;
this->set_interval(RX8111_EVENT_INTERVAL_ID, this->event_poll_interval_, [this]() { this->check_event_flags_(); });
this->event_polling_started_ = true;
}
void RX8111Component::update() {
this->read_time();
this->check_event_flags_();
}
void RX8111Component::set_warning_(uint8_t bit, const LogString *message) {
this->warnings_ |= bit;
this->status_set_warning(message);
}
void RX8111Component::clear_warning_(uint8_t bit) {
if ((this->warnings_ & bit) == 0)
return;
this->warnings_ &= ~bit;
if (this->warnings_ == 0)
this->status_clear_warning();
}
void RX8111Component::dump_config() {
ESP_LOGCONFIG(TAG, "RX8111:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG,
" Alarm interrupt: %s\n"
" Wake-up timer: %s",
ONOFF(this->alarm_enabled_), ONOFF(this->timer_enabled_));
if (this->timer_enabled_) {
ESP_LOGCONFIG(TAG, " Count: %" PRIu32 ", clock: %s", this->timer_count_,
LOG_STR_ARG(TimerClockNames::get_log_str(this->timer_clock_, TimerClockNames::LAST_INDEX)));
}
RealTimeClock::dump_config();
}
void RX8111Component::read_time() {
ESPTime rtc_time;
if (!this->read_calendar_(rtc_time))
return;
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
rtc_time.hour, rtc_time.minute, rtc_time.second);
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
}
bool RX8111Component::read_calendar_(ESPTime &rtc_time) {
uint8_t flags;
if (!this->read_byte(RX8111_REG_FLAG, &flags)) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
this->set_warning_(WARN_CALENDAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
if (flags & (RX8111_FLAG_VLF | RX8111_FLAG_XST | RX8111_FLAG_POR)) {
ESP_LOGW(TAG, "RTC time invalidated by power loss or oscillator stop; write trusted time");
this->status_set_warning(LOG_STR("RTC time invalidated; write trusted time"));
return;
this->set_warning_(WARN_CALENDAR, LOG_STR("RTC time invalidated; write trusted time"));
return false;
}
uint8_t buf[7];
// I2C access freezes the RX8111 counter, yielding a coherent calendar snapshot.
if (!this->read_bytes(RX8111_REG_SEC, buf, sizeof(buf))) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
this->set_warning_(WARN_CALENDAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
// decode_bcd_calendar() masks the same bits; the check here catches a corrupted register
static constexpr uint8_t CALENDAR_MASKS[7] = {0x7F, 0x7F, 0x3F, 0x00, 0x3F, 0x1F, 0xFF};
bool valid = true;
for (size_t i = 0; i < sizeof(buf); i++) {
if (i == 3)
continue; // Weekday uses one-hot encoding, not BCD.
buf[i] &= CALENDAR_MASKS[i];
if ((buf[i] & 0x0F) > 9 || (buf[i] >> 4) > 9) {
ESP_LOGW(TAG, "Invalid BCD in RTC time");
this->status_set_warning(LOG_STR("Invalid RTC time"));
return;
}
valid = valid && (buf[i] & 0x0F) <= 9 && (buf[i] >> 4) <= 9;
}
const uint8_t onehot = buf[3];
if (onehot == 0 || onehot > 0x40 || (onehot & (onehot - 1)) != 0) {
ESP_LOGW(TAG, "Invalid RTC weekday");
this->status_set_warning(LOG_STR("Invalid RTC time"));
return;
valid = valid && onehot != 0 && onehot <= 0x40 && (onehot & (onehot - 1)) == 0;
if (valid) {
rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(buf);
valid = rtc_time.is_valid(true, false) && rtc_time.second < 60 &&
ESPTime::from_epoch_utc(rtc_time.timestamp).day_of_week == rtc_time.day_of_week;
}
ESPTime rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(buf);
if (!rtc_time.is_valid(true, false) || rtc_time.second >= 60) {
if (!valid) {
ESP_LOGW(TAG, "Invalid RTC time, not syncing to system clock");
this->status_set_warning(LOG_STR("Invalid RTC time"));
return;
this->set_warning_(WARN_CALENDAR, LOG_STR("Invalid RTC time"));
return false;
}
if (!this->status_comm_result(ESPTime::from_epoch_utc(rtc_time.timestamp).day_of_week == rtc_time.day_of_week,
LOG_STR("Invalid RTC time"))) {
ESP_LOGW(TAG, "RTC weekday does not match calendar date");
return;
}
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
rtc_time.hour, rtc_time.minute, rtc_time.second);
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
this->clear_warning_(WARN_CALENDAR);
return true;
}
void RX8111Component::write_time() {
@@ -83,7 +230,7 @@ void RX8111Component::write_time() {
return;
}
if (!this->set_stop_bit_(true)) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
@@ -93,25 +240,233 @@ void RX8111Component::write_time() {
// Always try to restart the counter, even after a failed calendar write.
const bool restarted = this->set_stop_bit_(false);
if (!written || !restarted) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (!this->status_comm_result(this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS)))
if (!this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS)) {
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->clear_warning_(WARN_WRITE_TIME);
this->clear_warning_(WARN_CALENDAR);
ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
now.minute, now.second);
}
bool RX8111Component::set_stop_bit_(bool stop) {
uint8_t control;
if (!this->read_byte(RX8111_REG_CONTROL, &control))
return this->update_register(RX8111_REG_CONTROL, RX8111_CTRL_STOP, stop ? RX8111_CTRL_STOP : 0);
}
bool RX8111Component::set_alarm_interrupt_(bool enabled) {
return this->update_register(RX8111_REG_CONTROL, RX8111_CONTROL_AIE, enabled ? RX8111_CONTROL_AIE : 0);
}
bool RX8111Component::program_alarm_(uint8_t second, uint8_t minute, uint8_t hour, uint8_t weekday, uint8_t day,
bool boot) {
uint8_t extension;
if (!this->read_byte(RX8111_REG_EXTENSION, &extension))
return false;
if (stop) {
control |= RX8111_CTRL_STOP;
} else {
control &= ~RX8111_CTRL_STOP;
if (weekday != ALARM_FIELD_UNSET) {
extension &= ~RX8111_EXTENSION_WADA;
} else if (day != ALARM_FIELD_UNSET) {
extension |= RX8111_EXTENSION_WADA;
}
return this->write_byte(RX8111_REG_CONTROL, control);
const uint8_t fields[] = {
second == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(second),
minute == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(minute),
hour == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(hour),
weekday != ALARM_FIELD_UNSET ? static_cast<uint8_t>(1U << (weekday - 1))
: day != ALARM_FIELD_UNSET ? uint8_to_bcd(day)
: RX8111_ALARM_DISABLED,
};
// Disable the interrupt during partial programming; keep AF (past events) latched.
if (!this->set_alarm_interrupt_(false))
return false;
this->alarm_armed_ = false;
uint8_t flags;
if (!this->read_byte(RX8111_REG_FLAG, &flags))
return false;
if (!boot)
this->alarm_flag_seen_ = (flags & RX8111_FLAG_AF) != 0;
if (!this->write_byte(RX8111_REG_SEC_ALARM, fields[0]) || !this->write_bytes(RX8111_REG_MIN_ALARM, &fields[1], 3) ||
!this->write_byte(RX8111_REG_EXTENSION, extension))
return false;
if (!this->set_alarm_interrupt_(this->alarm_enabled_))
return false;
this->alarm_armed_ = true;
return true;
}
bool RX8111Component::set_alarm(uint16_t second, uint16_t minute, uint16_t hour, uint16_t weekday, uint16_t day) {
if (!this->alarm_enabled_) {
ESP_LOGE(TAG, "Alarm interrupt disabled; set alarm_enabled: true");
return false;
}
constexpr uint16_t unset = ALARM_FIELD_UNSET;
if ((second != unset && second > 59) || (minute != unset && minute > 59) || (hour != unset && hour > 23) ||
(weekday != unset && (weekday < 1 || weekday > 7)) || (day != unset && (day < 1 || day > 31)) ||
(weekday != unset && day != unset) ||
(second == unset && minute == unset && hour == unset && weekday == unset && day == unset)) {
ESP_LOGE(TAG, "Invalid alarm fields");
return false;
}
this->cancel_boot_retry_(true);
if (!this->program_alarm_(second, minute, hour, weekday, day)) {
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->clear_warning_(WARN_ALARM);
this->boot_alarm_event_seen_ = true;
this->start_event_polling_();
return true;
}
bool RX8111Component::schedule_alarm_in(uint32_t seconds) {
if (!this->alarm_enabled_ || seconds == 0 || seconds >= 28U * 86400U) {
ESP_LOGE(TAG, "Relative alarm requires alarm_enabled and a duration of 1 second to less than 28 days");
return false;
}
ESPTime now;
if (!this->read_calendar_(now)) {
ESP_LOGE(TAG, "Cannot read valid RTC time for relative alarm");
return false;
}
auto target = ESPTime::from_epoch_utc(now.timestamp + seconds);
if (!target.is_valid() || target.year > 2099) {
ESP_LOGE(TAG, "Relative alarm target is outside RTC year range");
return false;
}
return this->set_alarm(target.second, target.minute, target.hour, ALARM_FIELD_UNSET, target.day_of_month);
}
bool RX8111Component::unset_alarm() {
this->cancel_boot_retry_(true);
if (!this->set_alarm_interrupt_(false)) {
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->alarm_armed_ = false;
// Minute, hour and day alarm registers are contiguous
static constexpr uint8_t DISABLED_FIELDS[3] = {RX8111_ALARM_DISABLED, RX8111_ALARM_DISABLED, RX8111_ALARM_DISABLED};
if (!this->write_byte(RX8111_REG_SEC_ALARM, RX8111_ALARM_DISABLED) ||
!this->write_bytes(RX8111_REG_MIN_ALARM, DISABLED_FIELDS, sizeof(DISABLED_FIELDS))) {
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->clear_warning_(WARN_ALARM);
return true;
}
bool RX8111Component::clear_alarm_flag() {
if (!this->write_byte(RX8111_REG_FLAG, static_cast<uint8_t>(~RX8111_FLAG_AF))) {
this->set_warning_(WARN_ALARM_FLAG_CLEAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->alarm_flag_seen_ = false;
this->clear_warning_(WARN_ALARM_FLAG_CLEAR);
return true;
}
// Clears TE and TIE; returns the register values read so callers can build on them
bool RX8111Component::disable_timer_(uint8_t &extension, uint8_t &control) {
return this->read_byte(RX8111_REG_EXTENSION, &extension) &&
this->write_byte(RX8111_REG_EXTENSION, extension & ~RX8111_EXTENSION_TE) &&
this->read_byte(RX8111_REG_CONTROL, &control) &&
this->write_byte(RX8111_REG_CONTROL, control & ~RX8111_CONTROL_TIE);
}
bool RX8111Component::configure_timer_(bool clear_stale_flag) {
// Stop counting before updating the 24-bit reload value; preserve the boot flag unless explicitly starting.
uint8_t extension;
uint8_t control;
if (!this->disable_timer_(extension, control))
return false;
const uint8_t count[] = {static_cast<uint8_t>(this->timer_count_), static_cast<uint8_t>(this->timer_count_ >> 8),
static_cast<uint8_t>(this->timer_count_ >> 16)};
if (!this->write_bytes(RX8111_REG_TIMER0, count, sizeof(count)) ||
!this->write_byte(RX8111_REG_EXTENSION,
(extension & ~(RX8111_EXTENSION_TE | RX8111_EXTENSION_TSEL)) | this->timer_clock_) ||
(clear_stale_flag && !this->clear_timer_flag()) ||
!this->write_byte(RX8111_REG_CONTROL, control | RX8111_CONTROL_TIE))
return false;
return this->write_byte(RX8111_REG_EXTENSION,
(extension & ~RX8111_EXTENSION_TSEL) | this->timer_clock_ | RX8111_EXTENSION_TE);
}
bool RX8111Component::start_timer() {
if (this->timer_count_ == 0 || this->timer_count_ > 0xFFFFFF) {
ESP_LOGW(TAG, "Cannot start timer without a valid timer_count");
return false;
}
this->cancel_boot_retry_(false);
if (!this->configure_timer_(true)) {
// A partial configuration can leave either enable bit set. Disable both if the bus permits it;
// otherwise keep watching a timer that may still be running.
uint8_t extension, control;
if (this->disable_timer_(extension, control))
this->timer_enabled_ = false;
this->set_warning_(WARN_TIMER, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->clear_warning_(WARN_TIMER);
this->timer_enabled_ = true;
this->timer_retained_event_pending_ = false;
this->start_event_polling_();
return true;
}
bool RX8111Component::clear_timer_flag() {
if (!this->write_byte(RX8111_REG_FLAG, static_cast<uint8_t>(~RX8111_FLAG_TF))) {
this->set_warning_(WARN_TIMER_FLAG_CLEAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->timer_flag_seen_ = false;
this->timer_retained_event_pending_ = false;
this->clear_warning_(WARN_TIMER_FLAG_CLEAR);
return true;
}
bool RX8111Component::stop_timer() {
this->cancel_boot_retry_(false);
uint8_t extension, control;
if (!this->disable_timer_(extension, control)) {
this->set_warning_(WARN_TIMER, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->timer_enabled_ = false;
this->clear_warning_(WARN_TIMER);
return true;
}
bool RX8111Component::poll_event_flags_() {
uint8_t flags;
if (!this->read_byte(RX8111_REG_FLAG, &flags))
return false;
this->clear_warning_(WARN_EVENT_POLL);
const bool alarm_active = (flags & RX8111_FLAG_AF) != 0;
const bool timer_active = (flags & RX8111_FLAG_TF) != 0;
// alarm_armed_ is only ever set while the alarm is enabled
const bool timer_watched = this->timer_enabled_ || this->timer_retained_event_pending_;
if (alarm_active && this->alarm_armed_)
this->boot_alarm_event_seen_ = true;
const bool alarm_rising = alarm_active && !this->alarm_flag_seen_ && this->alarm_armed_;
const bool timer_rising = timer_active && !this->timer_flag_seen_ && timer_watched;
const bool deliver_stopped_event = timer_rising && this->timer_retained_event_pending_;
// Record eligible edges before callbacks, which may clear flags synchronously.
// Leave flags unseen while boot configuration is still waiting to enable delivery.
if (this->alarm_armed_)
this->alarm_flag_seen_ = alarm_active;
if (timer_watched)
this->timer_flag_seen_ = timer_active;
this->timer_retained_event_pending_ = false;
if (alarm_rising)
this->alarm_callbacks_.call();
// An alarm callback may stop the timer before its callback is delivered.
if (timer_rising && this->timer_flag_seen_ && (this->timer_enabled_ || deliver_stopped_event))
this->timer_callbacks_.call();
return true;
}
} // namespace esphome::rx8111
+87
View File
@@ -1,10 +1,15 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/time/real_time_clock.h"
namespace esphome::rx8111 {
namespace testing {
class RX8111Test;
} // namespace testing
class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice {
public:
void setup() override;
@@ -12,9 +17,91 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
void dump_config() override;
void read_time();
void write_time();
void set_alarm_enabled(bool enabled) { this->alarm_enabled_ = enabled; }
void set_alarm_explicitly_configured(bool configured) { this->alarm_explicitly_configured_ = configured; }
void set_alarm_second(uint8_t value) { this->alarm_second_ = value; }
void set_alarm_minute(uint8_t value) { this->alarm_minute_ = value; }
void set_alarm_hour(uint8_t value) { this->alarm_hour_ = value; }
void set_alarm_weekday(uint8_t value) { this->alarm_weekday_ = value; }
void set_alarm_day(uint8_t value) { this->alarm_day_ = value; }
void set_timer_count(uint32_t count) { this->timer_count_ = count; }
void set_timer_clock(uint8_t clock) { this->timer_clock_ = clock; }
void set_timer_enabled(bool enabled) { this->timer_enabled_ = enabled; }
void set_timer_configured(bool configured) { this->timer_configured_ = configured; }
void set_event_poll_interval(uint32_t interval) { this->event_poll_interval_ = interval; }
template<typename F> void add_on_alarm_callback(F &&callback) {
this->alarm_callbacks_.add(std::forward<F>(callback));
}
bool set_alarm(uint16_t second, uint16_t minute, uint16_t hour, uint16_t weekday, uint16_t day);
bool schedule_alarm_in(uint32_t seconds);
bool unset_alarm();
bool clear_alarm_flag();
bool clear_timer_flag();
bool start_timer();
bool stop_timer();
template<typename F> void add_on_timer_callback(F &&callback) {
this->timer_callbacks_.add(std::forward<F>(callback));
}
protected:
friend class testing::RX8111Test;
static constexpr uint32_t BOOT_RETRY_DELAY_MS = 1000;
// set_alarm() and the boot alarm ignore a field set to this value
static constexpr uint8_t ALARM_FIELD_UNSET = 0xFF;
// One bit per fault source, so a recovery in one path cannot hide another path's fault
enum Warning : uint8_t {
WARN_CALENDAR = 1 << 0,
WARN_EVENT_POLL = 1 << 1,
WARN_ALARM = 1 << 2,
WARN_TIMER = 1 << 3,
WARN_WRITE_TIME = 1 << 4,
WARN_ALARM_FLAG_CLEAR = 1 << 5,
WARN_TIMER_FLAG_CLEAR = 1 << 6,
};
bool set_stop_bit_(bool stop);
bool read_calendar_(ESPTime &rtc_time);
void set_warning_(uint8_t bit, const LogString *message);
void clear_warning_(uint8_t bit);
bool watching_() const { return this->alarm_armed_ || this->timer_enabled_ || this->timer_retained_event_pending_; }
void check_event_flags_();
bool program_alarm_(uint8_t second, uint8_t minute, uint8_t hour, uint8_t weekday, uint8_t day, bool boot = false);
bool set_alarm_interrupt_(bool enabled);
bool disable_timer_(uint8_t &extension, uint8_t &control);
bool configure_timer_(bool clear_stale_flag = false);
void start_event_polling_();
bool poll_event_flags_();
void configure_boot_alarm_();
void configure_boot_timer_();
void boot_config_result_(bool alarm, bool success);
void cancel_boot_retry_(bool alarm);
LazyCallbackManager<void()> alarm_callbacks_;
LazyCallbackManager<void()> timer_callbacks_;
uint32_t timer_count_{0};
uint32_t event_poll_interval_{1000};
uint32_t boot_retry_delay_ms_{BOOT_RETRY_DELAY_MS};
uint8_t warnings_{0};
uint8_t timer_clock_{2}; // 1 Hz
uint8_t boot_alarm_attempts_{0};
uint8_t boot_timer_attempts_{0};
uint8_t alarm_second_{ALARM_FIELD_UNSET};
uint8_t alarm_minute_{ALARM_FIELD_UNSET};
uint8_t alarm_hour_{ALARM_FIELD_UNSET};
uint8_t alarm_weekday_{ALARM_FIELD_UNSET};
uint8_t alarm_day_{ALARM_FIELD_UNSET};
bool timer_enabled_{false};
bool timer_configured_{false};
bool timer_flag_seen_{false};
bool timer_retained_event_pending_{false};
bool event_polling_started_{false};
bool boot_alarm_event_seen_{false};
bool alarm_enabled_{false};
bool alarm_explicitly_configured_{false};
bool alarm_flag_seen_{false};
bool alarm_armed_{false};
};
} // namespace esphome::rx8111
+177 -6
View File
@@ -4,6 +4,14 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c, time
import esphome.config_validation as cv
from esphome.const import (
CONF_DAY,
CONF_DURATION,
CONF_HOUR,
CONF_ID,
CONF_MINUTE,
CONF_SECOND,
)
from esphome.types import ConfigType
CODEOWNERS = ["@techdregs"]
@@ -11,23 +19,186 @@ DEPENDENCIES = ["i2c"]
rx8111_ns = cg.esphome_ns.namespace("rx8111")
RX8111Component = rx8111_ns.class_("RX8111Component", time.RealTimeClock, i2c.I2CDevice)
TIMER_CLOCKS = {"4096hz": 0, "64hz": 1, "1hz": 2, "1_60hz": 3}
# set_alarm() ignores a field set to this value
ALARM_FIELD_UNSET = 0xFF
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(RX8111Component)}
).extend(i2c.i2c_device_schema(0x32))
CONF_ALARM_SECOND = "alarm_second"
CONF_ALARM_MINUTE = "alarm_minute"
CONF_ALARM_HOUR = "alarm_hour"
CONF_ALARM_WEEKDAY = "alarm_weekday"
CONF_ALARM_DAY = "alarm_day"
CONF_ALARM_ENABLED = "alarm_enabled"
CONF_ON_ALARM = "on_alarm"
CONF_TIMER_COUNT = "timer_count"
CONF_TIMER_CLOCK = "timer_clock"
CONF_TIMER_ENABLED = "timer_enabled"
CONF_ON_TIMER = "on_timer"
CONF_WEEKDAY = "weekday"
CONF_EVENT_POLL_INTERVAL = "event_poll_interval"
_ALARM_FIELDS = (
CONF_ALARM_SECOND,
CONF_ALARM_MINUTE,
CONF_ALARM_HOUR,
CONF_ALARM_WEEKDAY,
CONF_ALARM_DAY,
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_ALARM, "add_on_alarm_callback"),
automation.CallbackAutomation(CONF_ON_TIMER, "add_on_timer_callback"),
)
def _validate_alarm_config(config: ConfigType) -> ConfigType:
"""Rules that depend on the derived alarm_enabled and timer_enabled defaults."""
if CONF_ON_ALARM in config and not config.get(
CONF_ALARM_ENABLED, any(f in config for f in _ALARM_FIELDS)
):
raise cv.Invalid(
"on_alarm requires alarm_enabled: true or alarm matching fields"
)
if CONF_TIMER_COUNT not in config and (
config.get(CONF_TIMER_ENABLED) or CONF_ON_TIMER in config
):
raise cv.Invalid("timer_enabled or on_timer requires timer_count")
return config
_validate_alarm_options = cv.All(
cv.has_at_most_one_key(CONF_ALARM_DAY, CONF_ALARM_WEEKDAY), _validate_alarm_config
)
CONFIG_SCHEMA = cv.All(
time.TIME_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(RX8111Component),
cv.Optional(CONF_ALARM_SECOND): cv.int_range(min=0, max=59),
cv.Optional(CONF_ALARM_MINUTE): cv.int_range(min=0, max=59),
cv.Optional(CONF_ALARM_HOUR): cv.int_range(min=0, max=23),
cv.Optional(CONF_ALARM_WEEKDAY): cv.int_range(min=1, max=7),
cv.Optional(CONF_ALARM_DAY): cv.int_range(min=1, max=31),
cv.Optional(CONF_ALARM_ENABLED): cv.boolean,
cv.Optional(CONF_ON_ALARM): automation.validate_automation({}),
cv.Optional(CONF_TIMER_COUNT): cv.int_range(min=1, max=0xFFFFFF),
cv.Optional(CONF_TIMER_CLOCK, default="1hz"): cv.enum(
TIMER_CLOCKS, lower=True
),
cv.Optional(CONF_TIMER_ENABLED): cv.boolean,
cv.Optional(CONF_ON_TIMER): automation.validate_automation({}),
# Below 500 ms a scheduler interval costs more than it saves
cv.Optional(CONF_EVENT_POLL_INTERVAL, default="1s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=cv.TimePeriod(milliseconds=500)),
),
}
).extend(i2c.i2c_device_schema(0x32)),
_validate_alarm_options,
)
RX8111_ACTION_SCHEMA = automation.maybe_simple_id(
{cv.GenerateID(): cv.use_id(RX8111Component)}
)
automation.register_apply_action(
"rx8111.write_time", RX8111_ACTION_SCHEMA, automation.ApplyCall("write_time()")
# Actions that call one argument-less method on the component
for _method in (
"write_time",
"read_time",
"clear_alarm_flag",
"clear_timer_flag",
"unset_alarm",
"stop_timer",
"start_timer",
):
automation.register_apply_action(
f"rx8111.{_method}", RX8111_ACTION_SCHEMA, automation.ApplyCall(f"{_method}()")
)
_SET_ALARM_FIELDS = (CONF_SECOND, CONF_MINUTE, CONF_HOUR, CONF_WEEKDAY, CONF_DAY)
def _fill_unset_alarm_fields(config: ConfigType) -> ConfigType:
"""Marks the fields not given as unset, so set_alarm() always gets all five."""
for field in _SET_ALARM_FIELDS:
config.setdefault(field, ALARM_FIELD_UNSET)
return config
_validate_set_alarm = cv.All(
cv.has_at_least_one_key(*_SET_ALARM_FIELDS),
cv.has_at_most_one_key(CONF_WEEKDAY, CONF_DAY),
_fill_unset_alarm_fields,
)
automation.register_apply_action(
"rx8111.read_time", RX8111_ACTION_SCHEMA, automation.ApplyCall("read_time()")
"rx8111.set_alarm",
cv.All(
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(RX8111Component),
cv.Optional(CONF_SECOND): cv.templatable(cv.int_range(min=0, max=59)),
cv.Optional(CONF_MINUTE): cv.templatable(cv.int_range(min=0, max=59)),
cv.Optional(CONF_HOUR): cv.templatable(cv.int_range(min=0, max=23)),
cv.Optional(CONF_WEEKDAY): cv.templatable(cv.int_range(min=1, max=7)),
cv.Optional(CONF_DAY): cv.templatable(cv.int_range(min=1, max=31)),
}
),
_validate_set_alarm,
),
automation.ApplyCall(
"set_alarm({}, {}, {}, {}, {})",
(
(CONF_SECOND, cg.uint16),
(CONF_MINUTE, cg.uint16),
(CONF_HOUR, cg.uint16),
(CONF_WEEKDAY, cg.uint16),
(CONF_DAY, cg.uint16),
),
),
)
# The alarm registers hold a day of month, so the target must stay inside the current month cycle
_ALARM_DURATION = cv.All(
cv.positive_time_period_seconds,
cv.Range(
min=cv.TimePeriod(seconds=1), max=cv.TimePeriod(days=28), max_included=False
),
)
automation.register_apply_action(
"rx8111.schedule_alarm_in",
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(RX8111Component),
cv.Required(CONF_DURATION): cv.templatable(_ALARM_DURATION),
}
),
automation.ApplyField(CONF_DURATION, "schedule_alarm_in", cg.uint32),
)
async def to_code(config: ConfigType) -> None:
var = await i2c.new_i2c_component(config)
await time.register_time(var, config)
cg.add(var.set_event_poll_interval(config[CONF_EVENT_POLL_INTERVAL]))
cg.add_optional(var.set_alarm_second, config.get(CONF_ALARM_SECOND))
cg.add_optional(var.set_alarm_minute, config.get(CONF_ALARM_MINUTE))
cg.add_optional(var.set_alarm_hour, config.get(CONF_ALARM_HOUR))
cg.add_optional(var.set_alarm_weekday, config.get(CONF_ALARM_WEEKDAY))
cg.add_optional(var.set_alarm_day, config.get(CONF_ALARM_DAY))
cg.add(
var.set_alarm_enabled(
config.get(CONF_ALARM_ENABLED, any(f in config for f in _ALARM_FIELDS))
)
)
cg.add(var.set_alarm_explicitly_configured(CONF_ALARM_ENABLED in config))
if (count := config.get(CONF_TIMER_COUNT)) is not None:
cg.add(var.set_timer_count(count))
cg.add(var.set_timer_clock(config[CONF_TIMER_CLOCK]))
# Validation guarantees on_timer comes with a timer_count
cg.add(var.set_timer_enabled(config.get(CONF_TIMER_ENABLED, count is not None)))
cg.add(var.set_timer_configured(CONF_TIMER_ENABLED in config or count is not None))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@@ -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();
+2
View File
@@ -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;
+6 -11
View File
@@ -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
+2 -3
View File
@@ -18,6 +18,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3,
VARIANT_ESP32S31,
only_on_variant,
require_idf_version,
)
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
from esphome.config_helpers import filter_source_files_from_platform
@@ -147,8 +148,7 @@ def _validate_psram_dma(value: Any) -> bool:
if not value:
return value
return cv.All(
cv.only_on_esp32,
cv.only_with_framework("esp-idf"),
require_idf_version(cv.Version(5, 5, 3), "psram_dma"),
only_on_variant(
supported=[
VARIANT_ESP32C5,
@@ -159,7 +159,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)
+135 -13
View File
@@ -1,4 +1,6 @@
from esphome import automation
from collections.abc import Callable
from esphome import automation, preferences
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server, zigbee
@@ -8,13 +10,16 @@ from esphome.const import (
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID,
CONF_INITIAL_STATE,
CONF_INVERTED,
CONF_MQTT_ID,
CONF_ON_STATE,
CONF_ON_TURN_OFF,
CONF_ON_TURN_ON,
CONF_RESTORE_MODE,
CONF_RESTORE_STATE,
CONF_STATE,
CONF_STORAGE,
CONF_WEB_SERVER,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_OUTLET,
@@ -22,10 +27,14 @@ from esphome.const import (
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
RESTORE_STATE_INITIAL,
RESTORE_STATE_INVERT,
RESTORE_STATE_KEEP,
ApplyEntity,
SubEntities,
entity_duplicate_validator,
queue_entity_register,
restore_state_schema,
setup_device_class,
setup_entity,
)
@@ -47,6 +56,8 @@ SwitchPtr = Switch.operator("ptr")
ApplySwitch = switch_ns.class_("ApplySwitch", Switch)
SwitchRestoreMode = switch_ns.enum("SwitchRestoreMode")
RESTORE_MODE_STORAGE_FLASH_MASK = switch_ns.RESTORE_MODE_STORAGE_FLASH_MASK
RESTORE_MODE_STORAGE_RTC_MASK = switch_ns.RESTORE_MODE_STORAGE_RTC_MASK
RESTORE_MODES = {
"RESTORE_DEFAULT_OFF": SwitchRestoreMode.SWITCH_RESTORE_DEFAULT_OFF,
@@ -60,6 +71,115 @@ RESTORE_MODES = {
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True)
validate_restore_mode = cv.enum(RESTORE_MODES, upper=True, space="_")
def _validate_restore_state_storage(config: ConfigType) -> ConfigType:
if CONF_STORAGE in config and config[CONF_STATE] == RESTORE_STATE_INITIAL:
raise cv.Invalid(
"'storage' only applies when a saved state is restored (KEEP or INVERT)",
path=[CONF_STORAGE],
)
return config
def _validate_restore_state_state(value: object) -> str:
# light accepts ON/OFF here (restore, then force a state); a switch has no such mode, and
# YAML turns ON into True, so say what to use instead of asking for quotes
if isinstance(value, bool) or (
isinstance(value, str) and value.upper() in ("ON", "OFF")
):
raise cv.Invalid(
"a switch can't restore and then force ON or OFF; to always start in a fixed state, "
"use 'initial_state: ON' (or OFF) with 'restore_state: none'"
)
return cv.one_of(
RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL, upper=True
)(value)
_RESTORE_STATE_FIELDS_SCHEMA = cv.Schema(
{
cv.Optional(
CONF_STATE, default=RESTORE_STATE_KEEP
): _validate_restore_state_state,
# No default: when unset, the existing restore_mode storage applies (on ESP8266,
# restore_from_flash decides), as with wifi
cv.Optional(CONF_STORAGE): preferences.validate_storage,
}
)
_RESTORE_STATE_FIELDS_SCHEMA.add_extra(_validate_restore_state_storage)
# A mapping of `state` and `storage`, or the shorthands `all` (KEEP) and `none` (INITIAL)
RESTORE_STATE_SCHEMA = restore_state_schema(
_RESTORE_STATE_FIELDS_SCHEMA, {CONF_STATE: RESTORE_STATE_INITIAL}
)
def _validate_initial_state(config: ConfigType) -> ConfigType:
if CONF_INITIAL_STATE in config and CONF_RESTORE_MODE in config:
raise cv.Invalid(
"'initial_state' can't be combined with 'restore_mode', which already sets "
"the boot state; use 'restore_state' instead",
path=[CONF_INITIAL_STATE],
)
return config
# (restore_state state, initial_state) -> restore mode
_RESTORE_MODE_FOR_STATE = {
(RESTORE_STATE_KEEP, False): "RESTORE_DEFAULT_OFF",
(RESTORE_STATE_KEEP, True): "RESTORE_DEFAULT_ON",
(RESTORE_STATE_INVERT, False): "RESTORE_INVERTED_DEFAULT_OFF",
(RESTORE_STATE_INVERT, True): "RESTORE_INVERTED_DEFAULT_ON",
(RESTORE_STATE_INITIAL, False): "ALWAYS_OFF",
(RESTORE_STATE_INITIAL, True): "ALWAYS_ON",
}
_STATE_FOR_RESTORE_MODE = {
mode: state for (state, _), mode in _RESTORE_MODE_FOR_STATE.items()
}
def _apply_default_restore_mode(
default_restore_mode: str,
) -> Callable[[ConfigType], ConfigType]:
# cv.Exclusive has no default, so apply it here when no restore option is configured.
# With only initial_state set, a restoring default keeps restoring and initial_state
# supplies the state used when nothing is saved.
default = validate_restore_mode(default_restore_mode)
state = _STATE_FOR_RESTORE_MODE.get(str(default), RESTORE_STATE_INITIAL)
def validator(config: ConfigType) -> ConfigType:
if CONF_RESTORE_MODE in config or CONF_RESTORE_STATE in config:
return config
if CONF_INITIAL_STATE in config:
config[CONF_RESTORE_STATE] = {CONF_STATE: state}
else:
config[CONF_RESTORE_MODE] = default
return config
return validator
def _resolve_restore_mode(config: ConfigType) -> MockObj | str:
"""Translate `initial_state` / `restore_state` into one of the RESTORE_MODES, with the storage bit."""
if (restore_mode := config.get(CONF_RESTORE_MODE)) is not None:
return restore_mode
restore_state = config.get(CONF_RESTORE_STATE, {})
state = restore_state.get(CONF_STATE, RESTORE_STATE_INITIAL)
mode = RESTORE_MODES[
_RESTORE_MODE_FOR_STATE[state, config.get(CONF_INITIAL_STATE, False)]
]
if (storage := restore_state.get(CONF_STORAGE)) is None:
return mode
cg.add_define("USE_SWITCH_STORAGE")
mask = (
RESTORE_MODE_STORAGE_FLASH_MASK
if preferences.is_in_flash(storage)
else RESTORE_MODE_STORAGE_RTC_MASK
)
return cg.RawExpression(f"static_cast<{SwitchRestoreMode}>({mode} | {mask})")
_SWITCH_SCHEMA = (
@@ -70,9 +190,9 @@ _SWITCH_SCHEMA = (
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent),
cv.Optional(CONF_INVERTED): cv.boolean,
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
RESTORE_MODES, upper=True, space="_"
),
cv.Exclusive(CONF_RESTORE_MODE, "restore"): validate_restore_mode,
cv.Exclusive(CONF_RESTORE_STATE, "restore"): RESTORE_STATE_SCHEMA,
cv.Optional(CONF_INITIAL_STATE): cv.boolean,
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation({}),
cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation({}),
@@ -86,6 +206,7 @@ _SWITCH_SCHEMA = (
_SWITCH_SCHEMA.add_extra(entity_duplicate_validator("switch"))
_SWITCH_SCHEMA.add_extra(zigbee.validate_switch)
_SWITCH_SCHEMA.add_extra(_validate_initial_state)
def switch_schema(
@@ -103,13 +224,6 @@ def switch_schema(
(CONF_DEVICE_CLASS, device_class, validate_device_class),
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_ICON, icon, cv.icon),
(
CONF_RESTORE_MODE,
default_restore_mode,
cv.enum(RESTORE_MODES, upper=True, space="_")
if default_restore_mode is not cv.UNDEFINED
else cv.UNDEFINED,
),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
@@ -119,7 +233,15 @@ def switch_schema(
"Inverted is not supported for this platform!"
)
return _SWITCH_SCHEMA.extend(schema)
result = _SWITCH_SCHEMA.extend(schema)
result.add_extra(
_apply_default_restore_mode(
"ALWAYS_OFF"
if default_restore_mode is cv.UNDEFINED
else default_restore_mode
)
)
return result
_CALLBACK_AUTOMATIONS = (
@@ -159,7 +281,7 @@ async def setup_switch_core_(var, config):
setup_device_class(config)
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
cg.add(var.set_restore_mode(_resolve_restore_mode(config)))
await zigbee.setup_switch(var, config)
+15 -1
View File
@@ -33,7 +33,14 @@ optional<bool> Switch::get_initial_state() {
if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK))
return {};
this->rtc_ = this->make_entity_preference<bool>();
#ifdef USE_SWITCH_STORAGE
if (restore_mode & RESTORE_MODE_STORAGE_MASK) {
this->rtc_ = this->make_entity_preference<bool>(0, restore_mode & RESTORE_MODE_STORAGE_FLASH_MASK);
} else
#endif
{
this->rtc_ = this->make_entity_preference<bool>();
}
bool initial_state;
if (!this->rtc_.load(&initial_state))
return {};
@@ -53,6 +60,13 @@ optional<bool> Switch::get_initial_state_with_restore_mode() {
}
return initial_state;
}
void Switch::restore() {
if (auto initial = this->get_initial_state_with_restore_mode())
this->control(*initial);
}
void Switch::restore(bool when_disabled) {
this->control(this->get_initial_state_with_restore_mode().value_or(when_disabled));
}
void Switch::publish_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
+13 -3
View File
@@ -14,11 +14,16 @@ namespace esphome::switch_ {
public: \
void set_##name##_switch(switch_::Switch *s) { this->name##_switch_ = s; }
// bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled
// bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled.
// bit4: restore from flash. bit5: restore from RTC memory (neither: platform default).
// Code generation ors the storage bit into the restore mode passed to set_restore_mode().
constexpr int RESTORE_MODE_ON_MASK = 0x01;
constexpr int RESTORE_MODE_PERSISTENT_MASK = 0x02;
constexpr int RESTORE_MODE_INVERTED_MASK = 0x04;
constexpr int RESTORE_MODE_DISABLED_MASK = 0x08;
constexpr int RESTORE_MODE_STORAGE_FLASH_MASK = 0x10;
constexpr int RESTORE_MODE_STORAGE_RTC_MASK = 0x20;
constexpr int RESTORE_MODE_STORAGE_MASK = RESTORE_MODE_STORAGE_FLASH_MASK | RESTORE_MODE_STORAGE_RTC_MASK;
enum SwitchRestoreMode : uint8_t {
SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK,
@@ -110,6 +115,11 @@ class Switch : public EntityBase {
*/
optional<bool> get_initial_state_with_restore_mode();
/// Drive the switch to the state its restore mode yields, through control(), so inversion and
/// write_state() apply; a disabled restore mode drives \p when_disabled, or nothing without one.
void restore();
void restore(bool when_disabled);
/** Return whether this switch uses an assumed state - i.e. if both the ON/OFF actions should be displayed in Home
* Assistant because the real state is unknown.
*
@@ -139,9 +149,9 @@ class Switch : public EntityBase {
LazyCallbackManager<void(bool)> state_callback_{};
// Small types grouped together
Deduplicator<bool> publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_)
Deduplicator<bool> publish_dedup_; // 3 bytes (bool has_value_ + bool value_unknown_ + bool last_value_)
bool inverted_{false}; // 1 byte
// Total: 3 bytes, 1 byte padding
// Total: 4 bytes, no padding
};
#define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj))
+5 -27
View File
@@ -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
-18
View File
@@ -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
+3 -26
View File
@@ -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
-19
View File
@@ -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
+24 -5
View File
@@ -92,9 +92,20 @@ void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) {
// If the pin is an output, write high/low
uint16_t temp_reg_data = 0;
this->read_byte_16(REG_DATA_B, &temp_reg_data);
assign_bits(this->output_state_, 1 << pin, bit_value);
// Outputs take the shadow state, inputs keep what was read
temp_reg_data = (temp_reg_data & this->ddr_mask_) | (this->output_state_ & ~this->ddr_mask_);
if (bit_value) {
output_state_ |= (1 << pin); // set bit in shadow register
} else {
output_state_ &= ~(1 << pin); // reset bit shadow register
}
for (uint16_t b = 0x8000; b; b >>= 1) {
if ((~ddr_mask_) & b) { // transfer bits of outputs, but don't mess with inputs
if (output_state_ & b) {
temp_reg_data |= b;
} else {
temp_reg_data &= ~b;
}
}
}
this->write_byte_16(REG_DATA_B, temp_reg_data);
}
}
@@ -137,12 +148,20 @@ void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) {
// Pullup
this->read_byte_16(REG_PULL_UP_B, &temp_word);
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLUP);
if (flags & gpio::FLAG_PULLUP) {
temp_word |= (1 << pin);
} else {
temp_word &= ~(1 << pin);
}
this->write_byte_16(REG_PULL_UP_B, temp_word);
// Pulldown
this->read_byte_16(REG_PULL_DOWN_B, &temp_word);
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLDOWN);
if (flags & gpio::FLAG_PULLDOWN) {
temp_word |= (1 << pin);
} else {
temp_word &= ~(1 << pin);
}
this->write_byte_16(REG_PULL_DOWN_B, temp_word);
// Set direction to input
+5 -2
View File
@@ -1,5 +1,4 @@
#include "tca9555.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00;
@@ -109,7 +108,11 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
this->status_comm_result(this->write_byte_16_le(TCA9555_OUTPUT_PORT_REGISTER_0, this->output_mask_),
LOG_STR("Failed to write output register"));
+52 -3
View File
@@ -1,3 +1,5 @@
import logging
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
@@ -9,6 +11,7 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_REACTIVE_ENERGY,
@@ -17,12 +20,15 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_HOUR,
UNIT_KILOVOLT_AMPS,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_MILLIAMP,
UNIT_MILLIVOLT,
UNIT_MINUTE,
UNIT_SECOND,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE_HOURS,
@@ -33,6 +39,8 @@ from esphome.types import ConfigType
from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, teleinfo_ns
_LOGGER = logging.getLogger(__name__)
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
@@ -59,6 +67,9 @@ _APPARENT_POWER_KVA = _preset(
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
)
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
_DURATION_MIN = _preset(UNIT_MINUTE, DEVICE_CLASS_DURATION, STATE_CLASS_MEASUREMENT)
# Numeric values without a physical unit (indexes, bit fields, versions)
_NO_UNIT: dict[str, str] = {}
# Legacy defaults, kept for tags without a known preset
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
@@ -75,6 +86,7 @@ _COMPATIBLE_UNITS = {
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
DEVICE_CLASS_DURATION: {UNIT_SECOND, UNIT_MINUTE, UNIT_HOUR},
}
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
@@ -90,6 +102,10 @@ TIC_TAG_CONFIGS = {
"CC": _POWER,
"PREF": _APPARENT_POWER_KVA,
"PCOUP": _APPARENT_POWER_KVA,
"VTIC": _NO_UNIT,
"NTARF": _NO_UNIT,
"NJOURF": _NO_UNIT,
"RELAIS": _NO_UNIT,
# Historical mode
"BASE": _ENERGY,
"HCH": _ENERGY,
@@ -101,15 +117,42 @@ TIC_TAG_CONFIGS = {
"ADPS": _CURRENT,
"ADIR": _CURRENT,
"PAPP": _APPARENT_POWER_VA,
"PMAX": _POWER,
"PMAX": _APPARENT_POWER_VA,
"PEJP": _DURATION_MIN,
}
# Prefixes of text tags, which a sensor cannot publish; a text_sensor should be used
# instead. They get no preset, and no prefix here is a prefix of a preset one either.
TIC_TEXT_TAGS = (
# Standard mode
"ADSC",
"DATE",
"NGTF",
"LTARF",
"STGE",
"DPM",
"FPM",
"MSG",
"PRM",
"PJOURF",
"PPOINTE",
# Historical mode
"ADCO",
"OPTARIF",
"PTEC",
"DEMAIN",
"HHPHC",
"MOTDETAT",
"PPOT",
)
def apply_tag_config(config: ConfigType) -> ConfigType:
"""Apply preset configurations based on the tag name.
Only keys not set by the user are filled in. For known tags, a preset unit or
device class is dropped if it is incompatible with the one set by the user.
Known text tags get no preset and log a warning, as a sensor cannot publish them.
Unknown tags get the legacy defaults, merged key by key as before.
"""
if CONF_TAG_NAME not in config:
@@ -117,6 +160,12 @@ def apply_tag_config(config: ConfigType) -> ConfigType:
# Runs before cv.string, so the raw value may not be a string yet
tag_name = str(config[CONF_TAG_NAME])
if tag_name.startswith(TIC_TEXT_TAGS):
_LOGGER.warning(
"TeleInfo tag '%s' holds a text value, use a text_sensor instead", tag_name
)
return config
preset = next(
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
None,
@@ -128,8 +177,8 @@ def apply_tag_config(config: ConfigType) -> ConfigType:
config.setdefault(key, value)
return config
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset.get(CONF_UNIT_OF_MEASUREMENT))
device_class = config.get(CONF_DEVICE_CLASS, preset.get(CONF_DEVICE_CLASS))
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
for key, value in preset.items():
+5 -11
View File
@@ -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)
-22
View File
@@ -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
+17 -17
View File
@@ -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)
-24
View File
@@ -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
+40 -78
View File
@@ -2,95 +2,57 @@
namespace esphome::unicode {
uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
// Safely cast to uint8_t* for correct bitwise operations on bytes
const uint8_t *current = reinterpret_cast<const uint8_t *>(utf8_str);
uint32_t code_point = 0;
uint8_t c1 = *current++;
// Reads count continuation bytes into code_point, stopping at the first byte that is not one so a
// sequence cut short by the terminator is never read past
static bool read_continuation(const uint8_t *&current, uint8_t count, uint32_t &code_point) {
for (uint8_t i = 0; i < count; i++) {
const uint8_t c = *current;
if ((c & 0xC0) != 0x80) {
return false;
}
current++;
code_point = (code_point << 6) | (c & 0x3F);
}
return true;
}
// check for end of string
uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
const auto *start = reinterpret_cast<const uint8_t *>(utf8_str);
const uint8_t *current = start;
const uint8_t c1 = *current++;
uint8_t count;
uint32_t code_point;
uint32_t lowest; // the smallest code point this sequence length may encode; shorter forms are overlong
if (c1 == 0) {
*length = 0;
return 0;
}
// --- 1-Byte Sequence: 0xxxxxxx (ASCII) ---
if (c1 < 0x80) {
// Valid ASCII byte.
count = 0;
code_point = c1;
// Optimization: No need to check for continuation bytes.
lowest = 0;
} else if ((c1 & 0xE0) == 0xC0) {
count = 1;
code_point = c1 & 0x1F;
lowest = 0x80;
} else if ((c1 & 0xF0) == 0xE0) {
count = 2;
code_point = c1 & 0x0F;
lowest = 0x800;
} else if ((c1 & 0xF8) == 0xF0) {
count = 3;
code_point = c1 & 0x07;
lowest = 0x10000;
} else {
*length = 0; // a continuation byte or an invalid lead byte
return 0;
}
// --- 2-Byte Sequence: 110xxxxx 10xxxxxx ---
else if ((c1 & 0xE0) == 0xC0) {
uint8_t c2 = *current++;
// Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx)
if ((c2 & 0xC0) != 0x80) {
*length = 0;
return 0;
}
code_point = (c1 & 0x1F) << 6;
code_point |= (c2 & 0x3F);
// Error Check 2: Overlong check (2-byte must be > 0x7F)
if (code_point <= 0x7F) {
*length = 0;
return 0;
}
}
// --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx ---
else if ((c1 & 0xF0) == 0xE0) {
uint8_t c2 = *current++;
uint8_t c3 = *current++;
// Error Check 1: Check continuation bytes
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) {
*length = 0;
return 0;
}
code_point = (c1 & 0x0F) << 12;
code_point |= (c2 & 0x3F) << 6;
code_point |= (c3 & 0x3F);
// Error Check 2: Overlong check (3-byte must be > 0x7FF)
// Also check for surrogates (0xD800-0xDFFF)
if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) {
*length = 0;
return 0;
}
}
// --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ---
else if ((c1 & 0xF8) == 0xF0) {
uint8_t c2 = *current++;
uint8_t c3 = *current++;
uint8_t c4 = *current++;
// Error Check 1: Check continuation bytes
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) {
*length = 0;
return 0;
}
code_point = (c1 & 0x07) << 18;
code_point |= (c2 & 0x3F) << 12;
code_point |= (c3 & 0x3F) << 6;
code_point |= (c4 & 0x3F);
// Error Check 2: Overlong check (4-byte must be > 0xFFFF)
// Also check for valid Unicode range (must be <= 0x10FFFF)
if (code_point <= 0xFFFF || code_point > 0x10FFFF) {
*length = 0;
return 0;
}
}
// --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) ---
else {
if (!read_continuation(current, count, code_point) || code_point < lowest || code_point > 0x10FFFF ||
(code_point >= 0xD800 && code_point <= 0xDFFF)) {
*length = 0;
return 0;
}
*length = current - reinterpret_cast<const uint8_t *>(utf8_str);
*length = current - start;
return code_point;
}
+4 -4
View File
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
get_esp32_variant,
idf_version,
only_on_variant,
require_idf_version,
)
import esphome.config_validation as cv
from esphome.const import CONF_DEVICES, CONF_FRAMEWORK, CONF_ID
@@ -201,10 +202,9 @@ def fifo_split() -> tuple[int, int, int] | None:
def _reconcile_fifo() -> None:
if (split := fifo_split()) is None:
return
if idf_version() < FIFO_CUSTOM_MIN_IDF:
raise cv.Invalid(
f"Sizing the USB FIFO on the {get_esp32_variant()} needs ESP-IDF {FIFO_CUSTOM_MIN_IDF} or later"
)
require_idf_version(
FIFO_CUSTOM_MIN_IDF, f"Sizing the USB FIFO on the {get_esp32_variant()}"
)(None)
rx, nptx, ptx = split
cg.add_define("USE_USB_HOST_FIFO_CONFIG")
cg.add_define("USB_HOST_FIFO_RX_LINES", rx)
+16 -4
View File
@@ -1,3 +1,5 @@
from collections.abc import Callable
import esphome.codegen as cg
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
@@ -154,6 +156,19 @@ FILTER_SOURCE_FILES = filter_source_files_from_defines(
)
def _channel_limit(type_: Type) -> Callable[[list], list]:
"""Check the channel count against the type's limit, which depends on the target."""
def validator(value: list) -> list:
if len(value) > (limit := type_.max_channels):
raise cv.Invalid(
f"Device type {type_.name} supports a maximum of {limit} channels"
)
return value
return validator
def channel_schema(type_: "Type") -> cv.Schema:
schema = cv.Schema(
{
@@ -192,10 +207,7 @@ def channel_schema(type_: "Type") -> cv.Schema:
}
)
),
cv.Length(
max=type_.max_channels,
msg=f"Device type {type_.name} supports a maximum of {type_.max_channels} channels",
),
_channel_limit(type_),
),
}
)
+18 -12
View File
@@ -1,25 +1,31 @@
#pragma once
#include <cstdint>
namespace esphome::valve {
class ValveTraits {
public:
ValveTraits() = default;
bool get_is_assumed_state() const { return this->is_assumed_state_; }
void set_is_assumed_state(bool is_assumed_state) { this->is_assumed_state_ = is_assumed_state; }
bool get_supports_position() const { return this->supports_position_; }
void set_supports_position(bool supports_position) { this->supports_position_ = supports_position; }
bool get_supports_toggle() const { return this->supports_toggle_; }
void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; }
bool get_supports_stop() const { return this->supports_stop_; }
void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; }
bool get_is_assumed_state() const { return this->flags_.is_assumed_state; }
void set_is_assumed_state(bool is_assumed_state) { this->flags_.is_assumed_state = is_assumed_state; }
bool get_supports_position() const { return this->flags_.supports_position; }
void set_supports_position(bool supports_position) { this->flags_.supports_position = supports_position; }
bool get_supports_toggle() const { return this->flags_.supports_toggle; }
void set_supports_toggle(bool supports_toggle) { this->flags_.supports_toggle = supports_toggle; }
bool get_supports_stop() const { return this->flags_.supports_stop; }
void set_supports_stop(bool supports_stop) { this->flags_.supports_stop = supports_stop; }
protected:
bool is_assumed_state_{false};
bool supports_position_{false};
bool supports_toggle_{false};
bool supports_stop_{false};
// Bit-packed flags to save memory (1 byte instead of 4)
struct ValveTraitFlags {
uint8_t is_assumed_state : 1;
uint8_t supports_position : 1;
uint8_t supports_toggle : 1;
uint8_t supports_stop : 1;
uint8_t reserved : 4;
} flags_{};
};
} // namespace esphome::valve
+14
View File
@@ -29,9 +29,15 @@ MULTI_CONF = True
CONF_MAX_UPDATE_DELAY = "max_update_delay"
CONF_UPDATE_DELAY = "update_delay"
CONF_PIXEL_FORMAT = "pixel_format"
vnc_ns = cg.esphome_ns.namespace("vnc")
VncClient = vnc_ns.class_("VncClient", cg.Component)
VncPixelFormat = vnc_ns.enum("VncPixelFormat", is_class=True)
PIXEL_FORMATS = VncPixelFormat.options(
"auto", "rgb565", "rgb332", prefix="VNC_PIXEL_FORMAT_"
)
# Drivers that cannot take another component's pixels yet.
UNSUPPORTED_DISPLAYS = dict.fromkeys(
@@ -63,6 +69,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_MAX_UPDATE_DELAY, default="1s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_PIXEL_FORMAT, default="auto"): cv.enum(
PIXEL_FORMATS, lower=True
),
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
}
@@ -128,6 +137,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
cg.add(var.set_update_delay(config[CONF_UPDATE_DELAY]))
cg.add(var.set_max_update_delay(config[CONF_MAX_UPDATE_DELAY]))
cg.add(var.set_pixel_format(config[CONF_PIXEL_FORMAT]))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@@ -135,4 +145,8 @@ VNC_ID_SCHEMA = automation.maybe_simple_id(
{cv.GenerateID(): cv.use_id(VncClient)},
)
automation.register_apply_action(
"vnc.refresh", VNC_ID_SCHEMA, automation.ApplyCall("refresh()")
)
automation.register_apply_condition("vnc.is_connected", VNC_ID_SCHEMA, "is_connected()")
+296 -11
View File
@@ -4,10 +4,12 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <iterator>
namespace esphome::vnc {
@@ -32,7 +34,19 @@ static constexpr uint8_t SECURITY_NONE = 1;
static constexpr uint8_t SECURITY_VNC_AUTH = 2;
static constexpr int32_t ENCODING_RAW = 0;
static constexpr int32_t ENCODING_HEXTILE = 5;
static constexpr int32_t ENCODING_RICH_CURSOR = -239;
// In preference order: TightVNC uses the first real encoding it supports for every rectangle.
static constexpr int32_t ENCODINGS[] = {ENCODING_HEXTILE, ENCODING_RAW, ENCODING_RICH_CURSOR};
// Indexed by VncPixelFormat
PROGMEM_STRING_TABLE(PixelFormatStrings, "auto", "RGB565", "RGB332");
static constexpr uint8_t HEXTILE_RAW = 0x01;
static constexpr uint8_t HEXTILE_BACKGROUND = 0x02;
static constexpr uint8_t HEXTILE_FOREGROUND = 0x04;
static constexpr uint8_t HEXTILE_ANY_SUBRECTS = 0x08;
static constexpr uint8_t HEXTILE_SUBRECTS_COLOURED = 0x10;
static constexpr uint8_t MSG_FRAMEBUFFER_UPDATE = 0;
static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
@@ -66,6 +80,32 @@ static int parse_three_digits(const uint8_t *p) {
void VncClient::setup() {
this->setup_ms_ = App.get_loop_component_start_time();
this->link_.begin(TAG);
// Sized for the display now; a rotation changed later swaps the sides but keeps the cell count.
const uint32_t wanted = static_cast<uint32_t>((this->display_->get_width() + 15) / 16) *
static_cast<uint32_t>((this->display_->get_height() + 15) / 16);
const uint32_t cells = std::min<uint32_t>(wanted, UINT16_MAX);
if (cells < wanted) {
ESP_LOGW(TAG, "Display has %" PRIu32 " tiles, more than the %u the cache holds", wanted, UINT16_MAX);
}
RAMAllocator<uint32_t> allocator;
this->tile_hashes_ = allocator.allocate(cells);
if (this->tile_hashes_ == nullptr) {
ESP_LOGW(TAG, "Not enough memory to skip unchanged tiles");
return;
}
this->hash_capacity_ = cells;
this->clear_tiles_();
}
void VncClient::refresh() {
this->need_full_ = true;
this->clear_tiles_();
}
void VncClient::clear_tiles_() {
if (this->tile_hashes_ != nullptr) {
std::memset(this->tile_hashes_, 0, sizeof(uint32_t) * this->hash_capacity_);
}
}
void VncClient::dump_config() {
@@ -74,9 +114,13 @@ void VncClient::dump_config() {
" Server: %s:%u\n"
" Reconnect Interval: %" PRIu32 "ms\n"
" Update Delay: %" PRIu32 "ms\n"
" Max Update Delay: %" PRIu32 "ms",
" Max Update Delay: %" PRIu32 "ms\n"
" Pixel Format: %s\n"
" Tile Cache: %u tiles",
this->link_.host(), this->link_.port(), this->link_.reconnect_interval(), this->update_delay_ms_,
this->max_update_delay_ms_);
this->max_update_delay_ms_,
LOG_STR_ARG(PixelFormatStrings::get_log_str(static_cast<uint8_t>(this->pixel_format_), 0)),
this->hash_capacity_);
}
void VncClient::on_shutdown() { this->link_.close(); }
@@ -95,12 +139,28 @@ void VncClient::loop() {
}
// A display that parks its loop when idle is busy while that loop runs. Displays that never
// park (no loop, or one that always runs) update synchronously, so they are never busy.
const bool was_busy = this->display_busy_;
if (this->display_->is_idle()) {
this->display_parks_ = true;
this->display_busy_ = false;
} else {
this->display_busy_ = this->display_parks_;
}
if (was_busy && !this->display_busy_) {
// A refresh the driver gave up on (it sets its warning) leaves the panel behind the buffer: retry it
// once and forget the cached tiles, which no longer say what the panel shows
if (!this->display_->status_has_warning()) {
this->refresh_retried_ = false;
} else if (!this->refresh_retried_) {
this->refresh_retried_ = true;
this->clear_tiles_();
if (!this->refresh_pending_) {
this->first_change_ms_ = now;
}
this->last_change_ms_ = now;
this->refresh_pending_ = true;
}
}
// A stuck panel, or another owner of the display's loop, would otherwise freeze the screen silently
if (!this->display_busy_) {
this->busy_since_ms_ = now;
@@ -147,6 +207,11 @@ void VncClient::loop() {
this->service_rx_(now);
}
if (this->state_ == VncState::VNC_STATE_RUNNING) {
// A new session lays the screen out again for the new rotation and redraws all of it.
if (static_cast<uint8_t>(this->display_->get_rotation() / 90) != this->hash_rotation_) {
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display rotation changed"));
return;
}
// Mid-frame there is nothing to send, so a vanished server would never fail a write.
if (this->in_update_ && !blocked && !this->rx_pending_ && now - this->last_rx_ms_ >= HEARTBEAT_MS) {
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Server stopped sending"));
@@ -183,12 +248,20 @@ void VncClient::connect_step_(uint32_t now) {
}
void VncClient::reset_session_() {
// Pixels drawn but never shown would match the cache when the next session resends them.
if (this->refresh_pending_ || this->frame_drawn_) {
this->clear_tiles_();
}
// Hextile colours carry from tile to tile; a new server must not inherit the last one's
std::memset(this->hex_bg_, 0, sizeof(this->hex_bg_));
std::memset(this->hex_fg_, 0, sizeof(this->hex_fg_));
this->rx_pos_ = 0;
this->rx_len_ = 0;
this->rx_pending_ = false;
this->in_update_ = false;
this->request_outstanding_ = false;
this->refresh_pending_ = false;
this->need_full_ = false;
this->frame_drawn_ = false;
this->step_ = VncStep::VNC_STEP_VERSION;
this->reason_failure_ = VncFailure::VNC_FAILURE_TRANSIENT;
@@ -478,6 +551,7 @@ VncParse VncClient::parse_() {
this->in_update_ = true;
this->update_start_ms_ = millis();
this->update_start_bytes_ = this->rx_total_ - (this->rx_len_ - this->rx_pos_);
this->drawn_tiles_ = 0;
this->rect_done_();
break;
case MSG_SET_COLOUR_MAP:
@@ -517,7 +591,7 @@ VncParse VncClient::parse_() {
const auto encoding = static_cast<int32_t>(read_be32(p + 8));
this->rx_pos_ += 12;
this->rects_left_--;
if (encoding == ENCODING_RAW) {
if (encoding == ENCODING_RAW || encoding == ENCODING_HEXTILE) {
if (static_cast<uint32_t>(this->rect_x_) + this->rect_w_ > this->fb_w_ ||
static_cast<uint32_t>(this->rect_y_) + this->rect_h_ > this->fb_h_) {
ESP_LOGW(TAG, "Rectangle %ux%u at %u,%u is outside the %ux%u screen", this->rect_w_, this->rect_h_,
@@ -526,10 +600,15 @@ VncParse VncClient::parse_() {
}
if (this->rect_w_ == 0 || this->rect_h_ == 0) {
this->rect_done_();
} else {
} else if (encoding == ENCODING_RAW) {
this->forget_tiles_(this->rect_x_, this->rect_y_, this->rect_w_, this->rect_h_);
this->pos_x_ = 0;
this->pos_y_ = 0;
this->step_ = VncStep::VNC_STEP_RAW;
} else {
this->pos_x_ = this->rect_x_;
this->pos_y_ = this->rect_y_;
this->step_ = VncStep::VNC_STEP_TILE;
}
} else if (encoding == ENCODING_RICH_CURSOR) {
if (this->rect_w_ > MAX_CURSOR_SIDE || this->rect_h_ > MAX_CURSOR_SIDE) {
@@ -570,6 +649,84 @@ VncParse VncClient::parse_() {
}
break;
}
case VncStep::VNC_STEP_TILE: {
if (this->display_busy_) {
return VncParse::VNC_PARSE_PAUSE;
}
if (avail < 1) {
return VncParse::VNC_PARSE_MORE;
}
if (this->budget_exhausted_()) {
return VncParse::VNC_PARSE_PAUSE;
}
const uint8_t flags = p[0];
this->tile_w_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_x_) + this->rect_w_ - this->pos_x_);
this->tile_h_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_y_) + this->rect_h_ - this->pos_y_);
this->tile_hidden_ = this->pos_x_ >= this->vis_w_ || this->pos_y_ >= this->vis_h_;
// With the raw bit set the other bits mean nothing and the pixels follow at once.
if (flags & HEXTILE_RAW) {
this->rx_pos_ += 1;
this->tile_fill_ = 0;
this->step_ = VncStep::VNC_STEP_TILE_RAW;
break;
}
const size_t need = 1 + ((flags & HEXTILE_BACKGROUND) ? this->bpp_ : 0) +
((flags & HEXTILE_FOREGROUND) ? this->bpp_ : 0) + ((flags & HEXTILE_ANY_SUBRECTS) ? 1 : 0);
if (avail < need) {
return VncParse::VNC_PARSE_MORE;
}
const uint8_t *q = p + 1;
if (flags & HEXTILE_BACKGROUND) {
std::memcpy(this->hex_bg_, q, this->bpp_);
q += this->bpp_;
}
if (flags & HEXTILE_FOREGROUND) {
std::memcpy(this->hex_fg_, q, this->bpp_);
q += this->bpp_;
}
this->subrects_left_ = (flags & HEXTILE_ANY_SUBRECTS) ? *q : 0;
this->subrects_coloured_ = (flags & HEXTILE_SUBRECTS_COLOURED) != 0;
this->rx_pos_ += need;
this->fill_tile_(0, 0, this->tile_w_, this->tile_h_, this->hex_bg_);
this->step_ = VncStep::VNC_STEP_SUBRECTS;
break;
}
case VncStep::VNC_STEP_TILE_RAW: {
const size_t total = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
const size_t count = std::min(avail, total - this->tile_fill_);
if (count == 0) {
return VncParse::VNC_PARSE_MORE;
}
std::memcpy(this->tile_ + this->tile_fill_, p, count);
this->rx_pos_ += count;
this->tile_fill_ += count;
if (this->tile_fill_ == total) {
this->draw_tile_();
this->next_tile_();
}
break;
}
case VncStep::VNC_STEP_SUBRECTS: {
if (this->subrects_left_ == 0) {
this->draw_tile_();
this->next_tile_();
break;
}
const size_t need = (this->subrects_coloured_ ? this->bpp_ : 0) + 2;
if (avail < need) {
return VncParse::VNC_PARSE_MORE;
}
const uint8_t *pixel = this->hex_fg_;
const uint8_t *q = p;
if (this->subrects_coloured_) {
pixel = q;
q += this->bpp_;
}
this->fill_tile_(q[0] >> 4, q[0] & 0x0F, (q[1] >> 4) + 1, (q[1] & 0x0F) + 1, pixel);
this->rx_pos_ += need;
this->subrects_left_--;
break;
}
case VncStep::VNC_STEP_SKIP: {
const auto count = static_cast<uint32_t>(std::min<size_t>(avail, this->skip_left_));
this->rx_pos_ += count;
@@ -592,6 +749,20 @@ void VncClient::rect_done_() {
}
}
void VncClient::next_tile_() {
this->step_ = VncStep::VNC_STEP_TILE;
if (static_cast<uint32_t>(this->pos_x_) + 16 < static_cast<uint32_t>(this->rect_x_) + this->rect_w_) {
this->pos_x_ += 16;
return;
}
this->pos_x_ = this->rect_x_;
if (static_cast<uint32_t>(this->pos_y_) + 16 < static_cast<uint32_t>(this->rect_y_) + this->rect_h_) {
this->pos_y_ += 16;
return;
}
this->rect_done_();
}
void VncClient::frame_done_() {
this->in_update_ = false;
this->request_outstanding_ = false;
@@ -604,9 +775,9 @@ void VncClient::frame_done_() {
}
this->last_change_ms_ = now;
this->refresh_pending_ = true;
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms",
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms, %u hextile tiles drawn",
this->rx_total_ - (this->rx_len_ - this->rx_pos_) - this->update_start_bytes_,
millis() - this->update_start_ms_);
millis() - this->update_start_ms_, this->drawn_tiles_);
}
}
@@ -619,8 +790,96 @@ void VncClient::draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t
this->frame_drawn_ = true;
}
void VncClient::draw_tile_() {
if (this->tile_hidden_) {
return;
}
const int w = std::min<int>(this->tile_w_, this->vis_w_ - this->pos_x_);
const int h = std::min<int>(this->tile_h_, this->vis_h_ - this->pos_y_);
uint32_t *cell = nullptr;
uint32_t hash = 0;
if (this->hash_cols_ != 0) {
if ((this->pos_x_ | this->pos_y_) & 15) {
// An off-grid tile spans several cells, and a later write to any of them would make its hash stale.
this->forget_tiles_(this->pos_x_, this->pos_y_, w, h);
} else {
cell = this->tile_hashes_ + (this->pos_y_ / 16) * this->hash_cols_ + this->pos_x_ / 16;
hash = this->tile_hash_();
if (*cell == hash) {
return;
}
}
}
this->display_->draw_pixels_at(this->pos_x_, this->pos_y_, w, h, this->tile_, this->bitness_, this->big_endian_, 0, 0,
this->tile_w_ - w);
this->frame_drawn_ = true;
this->drawn_tiles_++;
if (cell != nullptr) {
*cell = hash;
}
}
uint32_t VncClient::tile_hash_() const {
// The tile's size, pixel size and pixels; the cell gives its place. Zero means "unknown".
uint32_t hash = fnv1a_hash_extend(FNV1_OFFSET_BASIS,
static_cast<uint32_t>(this->tile_w_) << 16 | this->tile_h_ << 8 | this->bpp_);
const size_t len = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
size_t i = 0;
for (; i + 4 <= len; i += 4) {
uint32_t word;
std::memcpy(&word, this->tile_ + i, sizeof(word)); // tile_ is aligned, so this is one load
// Byte by byte: XOR-ing whole words lets high-bit changes in two words cancel.
hash = fnv1a_hash_extend(hash, word);
}
for (; i < len; i++) {
hash = fnv1a_hash_extend(hash, this->tile_[i]);
}
return hash == 0 ? 1 : hash;
}
void VncClient::forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
if (this->hash_cols_ == 0 || w == 0 || h == 0) {
return;
}
const uint16_t col_end = std::min<uint16_t>((x + w - 1) / 16, this->hash_cols_ - 1);
const uint16_t row_end = std::min<uint16_t>((y + h - 1) / 16, this->hash_rows_ - 1);
for (uint16_t row = y / 16; row <= row_end; row++) {
for (uint16_t col = x / 16; col <= col_end; col++) {
this->tile_hashes_[row * this->hash_cols_ + col] = 0;
}
}
}
void VncClient::fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel) {
if (this->tile_hidden_ || x >= this->tile_w_ || y >= this->tile_h_) {
return;
}
w = std::min<uint8_t>(w, this->tile_w_ - x);
h = std::min<uint8_t>(h, this->tile_h_ - y);
const size_t stride = static_cast<size_t>(this->tile_w_) * this->bpp_;
const size_t row_len = static_cast<size_t>(w) * this->bpp_;
uint8_t *first = this->tile_ + y * stride + x * this->bpp_;
if (this->bpp_ == 1) {
std::memset(first, pixel[0], w);
} else {
for (size_t i = 0; i < row_len; i += 2) {
first[i] = pixel[0];
first[i + 1] = pixel[1];
}
}
for (uint8_t row = 1; row < h; row++) {
std::memcpy(first + row * stride, first, row_len);
}
}
bool VncClient::start_session_(uint32_t now) {
const bool rgb565 = this->display_->get_display_type() != display::DISPLAY_TYPE_BINARY;
VncPixelFormat format = this->pixel_format_;
if (format == VncPixelFormat::VNC_PIXEL_FORMAT_AUTO) {
format = this->display_->get_display_type() == display::DISPLAY_TYPE_BINARY
? VncPixelFormat::VNC_PIXEL_FORMAT_RGB332
: VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
}
const bool rgb565 = format == VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
this->bpp_ = rgb565 ? 2 : 1;
this->bitness_ = rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_332;
@@ -632,6 +891,24 @@ bool VncClient::start_session_(uint32_t now) {
ESP_LOGW(TAG, "Server screen is %ux%u but the display is %dx%d; showing the top-left %ux%u", this->fb_w_,
this->fb_h_, disp_w, disp_h, this->vis_w_, this->vis_h_);
}
const uint16_t cols = (this->vis_w_ + 15) / 16;
const uint16_t rows = (this->vis_h_ + 15) / 16;
const auto rotation = static_cast<uint8_t>(this->display_->get_rotation() / 90);
const bool rotated = rotation != this->hash_rotation_;
this->hash_rotation_ = rotation;
if (this->tile_hashes_ == nullptr) {
this->hash_cols_ = 0;
this->hash_rows_ = 0;
} else if (static_cast<uint32_t>(cols) * rows > this->hash_capacity_) {
ESP_LOGW(TAG, "Screen has %u tiles, more than the %u cached; every tile is drawn", cols * rows,
this->hash_capacity_);
this->hash_cols_ = 0;
this->hash_rows_ = 0;
} else if (cols != this->hash_cols_ || rows != this->hash_rows_ || rotated) {
this->hash_cols_ = cols;
this->hash_rows_ = rows;
this->clear_tiles_();
}
// SetPixelFormat: true colour, in the byte order the display takes
uint8_t pixel_format[20] = {};
@@ -648,10 +925,12 @@ bool VncClient::start_session_(uint32_t now) {
return false;
}
// SetEncodings: TightVNC uses the first real encoding it supports for every rectangle.
uint8_t encodings[12] = {2, 0, 0, 2};
write_be32(encodings + 4, ENCODING_RAW);
write_be32(encodings + 8, static_cast<uint32_t>(ENCODING_RICH_CURSOR));
// SetEncodings
uint8_t encodings[4 + 4 * std::size(ENCODINGS)] = {2};
write_be16(encodings + 2, std::size(ENCODINGS));
for (size_t i = 0; i < std::size(ENCODINGS); i++) {
write_be32(encodings + 4 + 4 * i, static_cast<uint32_t>(ENCODINGS[i]));
}
if (!this->send_(encodings, sizeof(encodings)) || !this->send_update_request_(false, now)) {
return false;
}
@@ -669,6 +948,12 @@ void VncClient::pace_(uint32_t now) {
if (this->display_busy_ || this->in_update_ || now - this->last_request_ms_ < MIN_REQUEST_SPACING_MS) {
return;
}
if (this->need_full_) {
if (this->send_update_request_(false, now)) {
this->need_full_ = false;
}
return;
}
// Repeating a parked request costs nothing on the server and detects a dead link.
if (!this->request_outstanding_ ||
(now - this->last_rx_ms_ >= HEARTBEAT_MS && now - this->last_request_ms_ >= HEARTBEAT_MS)) {
+41 -1
View File
@@ -10,6 +10,12 @@
namespace esphome::vnc {
enum class VncPixelFormat : uint8_t {
VNC_PIXEL_FORMAT_AUTO,
VNC_PIXEL_FORMAT_RGB565,
VNC_PIXEL_FORMAT_RGB332,
};
enum class VncState : uint8_t {
VNC_STATE_IDLE,
VNC_STATE_HANDSHAKE,
@@ -31,6 +37,9 @@ enum class VncStep : uint8_t {
VNC_STEP_RECT,
VNC_STEP_RECT_DONE,
VNC_STEP_RAW,
VNC_STEP_TILE,
VNC_STEP_TILE_RAW,
VNC_STEP_SUBRECTS,
VNC_STEP_SKIP,
};
@@ -55,6 +64,7 @@ class VncClient : public Component {
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
void set_update_delay(uint32_t ms) { this->update_delay_ms_ = ms; }
void set_max_update_delay(uint32_t ms) { this->max_update_delay_ms_ = ms; }
void set_pixel_format(VncPixelFormat format) { this->pixel_format_ = format; }
void setup() override;
void loop() override;
@@ -63,6 +73,8 @@ class VncClient : public Component {
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
bool is_connected() const { return this->state_ == VncState::VNC_STATE_RUNNING; }
/// Ask the server for the whole screen again.
void refresh();
template<typename F> void add_on_connect_callback(F &&callback) {
this->connect_callback_.add(std::forward<F>(callback));
@@ -73,6 +85,7 @@ class VncClient : public Component {
protected:
static constexpr size_t RX_BUFFER_SIZE = 512;
static constexpr size_t TILE_BUFFER_SIZE = 16 * 16 * 2;
void connect_step_(uint32_t now);
void start_handshake_(uint32_t now);
@@ -83,7 +96,13 @@ class VncClient : public Component {
bool send_client_init_();
void frame_done_();
void rect_done_();
void next_tile_();
void draw_tile_();
uint32_t tile_hash_() const;
void clear_tiles_();
void forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
void draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels);
void fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel);
void skip_(uint32_t count, VncStep next);
bool budget_exhausted_() const;
bool send_(const uint8_t *data, size_t len);
@@ -100,6 +119,9 @@ class VncClient : public Component {
LazyCallbackManager<void()> connect_callback_;
LazyCallbackManager<void()> disconnect_callback_;
HighFrequencyLoopRequester high_freq_;
// Fingerprint of the tile last drawn at each 16x16 cell, so tiles the server resends unchanged are skipped.
// A 32-bit FNV-1a collision keeps a stale tile until it changes again or vnc.refresh runs; accepted.
uint32_t *tile_hashes_{nullptr};
uint32_t update_delay_ms_{200};
uint32_t max_update_delay_ms_{1000};
@@ -131,15 +153,28 @@ class VncClient : public Component {
uint16_t rect_y_{0};
uint16_t rect_w_{0};
uint16_t rect_h_{0};
// Raw: column and row inside the rect.
// Raw: column and row inside the rect. Hextile: absolute origin of the current tile.
uint16_t pos_x_{0};
uint16_t pos_y_{0};
uint16_t tile_fill_{0};
uint16_t drawn_tiles_{0};
uint16_t hash_capacity_{0};
uint16_t hash_cols_{0};
uint16_t hash_rows_{0};
uint8_t tile_w_{0};
uint8_t tile_h_{0};
uint8_t subrects_left_{0};
uint8_t bpp_{2};
uint8_t minor_{8};
uint8_t sec_count_{0};
uint8_t text_len_{0}; // clamped to MAX_TEXT_LEN
// The display rotation the cached tiles were drawn at, in quarter turns.
uint8_t hash_rotation_{0};
uint8_t hex_bg_[2]{};
uint8_t hex_fg_[2]{};
display::ColorBitness bitness_{display::COLOR_BITNESS_565};
VncPixelFormat pixel_format_{VncPixelFormat::VNC_PIXEL_FORMAT_AUTO};
VncState state_{VncState::VNC_STATE_IDLE};
VncStep step_{VncStep::VNC_STEP_VERSION};
VncStep after_skip_{VncStep::VNC_STEP_MESSAGE};
@@ -149,14 +184,19 @@ class VncClient : public Component {
bool in_update_{false};
bool request_outstanding_{false};
bool refresh_pending_{false};
bool need_full_{false};
bool frame_drawn_{false};
bool subrects_coloured_{false};
bool tile_hidden_{false};
bool display_parks_{false};
bool display_busy_{false};
bool busy_warned_{false};
bool refresh_retried_{false}; // one retry per refresh the driver abandoned
bool network_was_up_{false};
bool big_endian_; // the pixel order the display takes
uint8_t rx_[RX_BUFFER_SIZE];
alignas(4) uint8_t tile_[TILE_BUFFER_SIZE];
};
} // namespace esphome::vnc
@@ -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
@@ -1,5 +1,4 @@
#include "waveshare_io_ch32v003.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::waveshare_io_ch32v003 {
@@ -111,7 +110,11 @@ void WaveshareIOCH32V003Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
uint8_t data = this->output_mask_;
this->status_comm_result(this->write_byte(IO_EXTENSION_IO_OUTPUT_ADDR, data),
+18 -32
View File
@@ -5,6 +5,7 @@
#include "weikai.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
namespace esphome::weikai {
@@ -37,35 +38,34 @@ const char *p2s(uart::UARTParityOptions parity) {
}
/// @brief Display a buffer in hexadecimal format (32 hex values / line) for debug
void print_buffer(const uint8_t *data, size_t length) {
void print_buffer(const char *tag, const uint8_t *data, size_t length, int level) {
char hex_buffer[100];
hex_buffer[(3 * 32) + 1] = 0;
for (size_t i = 0; i < length; i++) {
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
if (i % 32 == 31) {
ESP_LOGVV(TAG, " %s", hex_buffer);
esp_log_printf_(level, tag, __LINE__, ESPHOME_LOG_FORMAT(" %s"), hex_buffer);
}
}
if (length % 32) {
// null terminate if incomplete line
hex_buffer[3 * (length % 32) + 2] = 0;
ESP_LOGVV(TAG, " %s", hex_buffer);
esp_log_printf_(level, tag, __LINE__, ESPHOME_LOG_FORMAT(" %s"), hex_buffer);
}
}
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
PROGMEM_STRING_TABLE(RegNamesPage0, "GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER", "SIFR", "TFCNT",
"RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485");
PROGMEM_STRING_TABLE(RegNamesPage1, "GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL", "TFTL", "FWTH",
"FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY");
// method to print a register value as text: used in the log messages ...
const char *reg_to_str(int reg, bool page1) {
const LogString *reg_to_str(int reg, bool page1) {
if (reg == WKREG_GPDAT) {
return "GPDAT";
return LOG_STR("GPDAT");
} else if (reg == WKREG_GPDIR) {
return "GPDIR";
return LOG_STR("GPDIR");
} else {
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
return page1 ? RegNamesPage1::get_log_str(reg & 0x0F, 0) : RegNamesPage0::get_log_str(reg & 0x0F, 0);
}
}
@@ -210,7 +210,11 @@ bool WeikaiComponent::read_pin_val_(uint8_t pin) {
}
void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
assign_bits(this->output_state_, 1 << pin, value);
if (value) {
this->output_state_ |= (1 << pin);
} else {
this->output_state_ &= ~(1 << pin);
}
char bin_buf[9];
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
format_bin_to(bin_buf, this->output_state_));
@@ -491,24 +495,6 @@ class Increment {
uint8_t i_;
};
/// @brief Hex converter to print/display a buffer in hexadecimal format (32 hex values / line).
/// @param buffer contains the values to display
void print_buffer(std::vector<uint8_t> buffer) {
char hex_buffer[100];
hex_buffer[(3 * 32) + 1] = 0;
for (size_t i = 0; i < buffer.size(); i++) {
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", buffer[i]);
if (i % 32 == 31) {
ESP_LOGI(TAG, " %s", hex_buffer);
}
}
if (buffer.size() % 32) {
// null terminate if incomplete line
hex_buffer[3 * (buffer.size() % 32) + 1] = 0;
ESP_LOGI(TAG, " %s", hex_buffer);
}
}
/// @brief test the write_array method
void WeikaiChannel::uart_send_test_(char *message) {
auto start_exec = micros();
@@ -556,7 +542,7 @@ bool WeikaiChannel::uart_receive_test_(char *message) {
for (size_t i = 0; i < RING_BUFFER_SIZE; i++) {
if (buffer[i] != i % XFER_MAX_SIZE) {
ESP_LOGE(TAG, "Read buffer contains error b=%x i=%x", buffer[i], i % XFER_MAX_SIZE);
print_buffer(buffer);
print_buffer(TAG, buffer.data(), buffer.size(), ESPHOME_LOG_LEVEL_INFO);
status = false;
break;
}

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