Compare commits

..
Author SHA1 Message Date
J. Nick Koston fef73dbd15 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 17:24:30 -10:00
J. Nick Koston e426e33427 [core] Assert a frame came back before each resync() in the FrameReader tests 2026-10-22 17:24:30 -10:00
J. Nick Koston 0628e4e492 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 16:44:21 -10:00
J. Nick Koston d5c6b01e19 [core] Cap FrameReader headers at 8 bytes to bound the replay depth 2026-10-22 16:44:21 -10:00
J. Nick Koston abbf3a6960 [sm300d2] Wait for the rest of a frame cut off at the end of the buffer 2026-10-22 16:44:12 -10:00
J. Nick Koston a97d4241bd [ds1603l] [ufm01] Read with read_available like the other FrameReader users 2026-10-22 16:33:33 -10:00
J. Nick Koston f1031bc64d [dfrobot_sen0465_uart] Read the answer with one read_available call 2026-10-22 16:31:31 -10:00
J. Nick Koston 57e14c1b54 [core] Wrap the AGENTS.md bus helpers lines at 120 characters 2026-10-22 16:13:15 -10:00
J. Nick Koston 71064efc25 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 16:13:15 -10:00
J. Nick Koston f5ea2d9f01 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 15:58:37 -10:00
J. Nick Koston 11a228569b [core] State when FrameReader::resync() may be called and list FrameReader in AGENTS.md 2026-10-22 15:58:36 -10:00
J. Nick Koston 5d69784adc [sm300d2] Read the frame with read_available and inline the checksum helper 2026-10-22 15:51:02 -10:00
J. Nick Koston 3b4deefd4c [core] Drop FrameReader's single byte header special case and deduce N in tests 2026-10-22 15:47:52 -10:00
J. Nick Koston 22a0440348 Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 15:47:52 -10:00
J. Nick Koston 46379d8cbc [ufm01] Brace the single log call body 2026-10-22 15:45:41 -10:00
J. Nick Koston f00328f45d Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 15:39:40 -10:00
J. Nick Koston ddfcc0bc8b [core] Name FrameReader's static header lookup without a trailing underscore 2026-10-22 15:39:36 -10:00
J. Nick Koston 61bd5e2657 [ufm01] Drop the start byte checks FrameReader already made 2026-10-22 15:22:36 -10:00
J. Nick Koston 7a1dc1279d [uart] Read bytes one at a time in the FrameReader users to keep flash down 2026-10-22 15:14:41 -10:00
J. Nick Koston 04165e289f Merge branch 'uart-frame-reader' into uart-frame-reader-migrate 2026-10-22 15:09:52 -10:00
J. Nick Koston 965b26dab6 [core] Realign a broken FrameReader header by replaying its bytes 2026-10-22 15:09:51 -10:00
J. Nick Koston a7e5a2a621 [uart] Collect fixed length frames with FrameReader in five components 2026-10-22 15:08:57 -10:00
J. Nick Koston 8824610f82 [core] Add FrameReader to collect fixed length binary frames 2026-10-22 15:04:08 -10:00
599 changed files with 5070 additions and 19701 deletions

No files matched your search

+2 -5
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, loopback harness driver with number and binary sensor states and TX frame summarizer), `api/` (raw plaintext native API client for user defined actions, store_yaml recovery client over Noise or plaintext), `http_request/` (update manifest server, update state driver), `modem/` (button and sensor driver over the plaintext API, also used for the BLE MIDI boards), `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`
@@ -132,10 +132,7 @@ capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `l
`globals-storage-dut.yaml`, `usb-audio-waveshare-s3-eth-dut.yaml`, `usb-audio-sendspin-s3-eth-dut.yaml`,
`cast-pro-1-sendspin-control.yaml`, `aux-2-preprod-stock-dut.yaml`, `aux-2-preprod-usb-audio-dut.yaml`,
`dfrobot-sen0395-dut.yaml`, `aux-2-preprod-speaker-player-dut.yaml`, `web-server-sse-dut.yaml`,
`mqtt-transport-dut.yaml`, `mqtt-tls-dut.yaml`, `udp-write-dut.yaml`, `entity-icon-dut.yaml`,
and the directories `store_yaml/` (package with secrets one directory up, per-board files),
`http_request_update/` (manifest URL setter shapes, per-board files), `modem/` (modem only and modem plus
Wi-Fi boot checks) and `ble_midi/` (fake BLE MIDI instrument and the client).
`mqtt-transport-dut.yaml`, `mqtt-tls-dut.yaml`, `udp-write-dut.yaml`, `entity-icon-dut.yaml`.
## When something looks wrong
@@ -4,17 +4,14 @@ 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, user defined action listing and execution with a raw plaintext client, store_yaml recovery over Noise or plaintext
- [`ble_midi.md`](ble_midi.md): BLE MIDI client against a fake ESP32 instrument, every packet shape and the send actions
- [`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
- [`http_request.md`](http_request.md): update manifest URL changed at run time, every setter shape, a swap under a running check
- [`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
- [`modem.md`](modem.md): PPPoS modem component boot check with no modem attached, loop blocking and disable timing
- [`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
- [`online_image.md`](online_image.md): stalled and malformed downloads
@@ -40,19 +40,3 @@ length delimited strings and `int[]` as repeated sint32 (field 7); `ALLOW_MISSIN
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.
## store_yaml recovery over the native API (#16445)
`scripts/api/syrecover.py HOST KEY OUTDIR [ORIGDIR]` (KEY `-` for plaintext) builds the client library's
`APINoiseFrameHelper` around a stub `APIConnection` subclass (the compiled helper type checks the connection, and
`ConnectionParams` needs every field), sends Hello, Authentication (never answered without a password) and
`DeviceCapabilitiesRequest`, then `GetYamlRequest` (161), collects `GetYamlResponse` (162) frames by field number
(1 data, 2 done, 3 total_size, 4 encoding) until done, requests again, decompresses with `compression.zstd` (Python 3.14, or the `zstandard` package) and
unpacks the envelope with the tree's `unpack_envelope` (run with `PYTHONPATH` on that tree). Configs in
`templates/store_yaml/`: entry files in `boards/`, a package one directory above them with `!secret` and an inline
OTA password,
so the redaction and the `..` paths are exercised. Repetitive YAML compresses into one chunk even at 22 KB; a 6 KB
include of random base64 lines with `include_secrets: true` gives four chunks (1390 byte packets). Rough edge at
0bd3738430: `include_secrets: true` fails the build when a package in another directory uses `!secret` and has no
`secrets.yaml` beside it; a copy next to the package works around it.
Results: https://github.com/esphome/esphome/pull/16445#issuecomment-6335435397
@@ -1,19 +0,0 @@
# ble_midi
## BLE MIDI client against a fake instrument (#18508)
No MIDI hardware needed: `templates/ble_midi/midiperi.yaml` turns a second ESP32 (an S3 with ethernet, so the
API stays reachable while BLE is busy) into a BLE MIDI peripheral with `esp32_ble_server`: the MIDI service
advertised, the MIDI characteristic with read, write, write without response and notify, an `on_write` that logs
the bytes, buttons that notify hand built packets (channel messages, two messages in one packet, running status,
SysEx in one packet, split over two, oversize, cut short, clock, aftertouch, orphan data bytes, a 50 packet burst)
and a restart button for the disconnect test. `midicli.yaml` is the component test config on a classic devkit with
`wifi:`, `api:`, the peripheral's BLE address in the `peri_mac` substitution, given on the command line with
`-s peri_mac <mac>` (the base MAC plus 2, printed by `esp32_ble` in `dump_config`), `on_sysex` and `on_message` lambdas that print the bytes, debug sensors and out of range send
buttons. Drive both over the plaintext API with `scripts/modem/mdrive.py HOST LOGFILE press:NAME wait:S` 3 s
apart and compare the peripheral's `TX` and `RX` lines with the client's decoded lines. Gotcha:
`ble_server.characteristic.set_value` latches the value of its first play and re-applies it before every notify,
so the peripheral script sets the value from a lambda (`id(midi_io).set_value(std::vector<uint8_t>(pkt));
id(midi_io).notify();`). At VERBOSE the client logs four lines per message, so a burst shows an
`esp32_ble took a long time` of a couple of seconds that is the serial port, not the parser.
Results: https://github.com/esphome/esphome/pull/18508#issuecomment-6348272717
@@ -1,18 +0,0 @@
# http_request
## Update manifest URL changed at run time (#21373)
`scripts/http_request/manifest_server.py PORT [DELAY]` serves `stable.json`, `beta.json` and `slow.json` (answered
after DELAY s) with both chip families and logs every request. `templates/http_request_update/upd-common.yaml`
(secret `manifest_base` is the server URL) has the update entity on the stable manifest, buttons for every
`set_source_url` shape (`std::string` local, literal, temporary, `.c_str()` of a local, the current URL back in),
`Check Then Swap` (ESP32 task path: check the slow manifest and replace the URL in the same lambda), `Arm Swap In
Check` plus a `logger: on_message` lambda that replaces the URL from inside the inline check (ESP8266; the log line
from inside `on_message` is swallowed, judge by the next check's result), and a `Heap` button. `upd-base-common.yaml`
keeps only the shapes that compile on a tree with the bare pointer setter (for the base build, copy a board file
and point its `common` include at `../upd-base-common.yaml` with a different `name:`); `boards/upd8266.yaml` and `boards/upd32.yaml` are the
per-board files, one directory below the package their `../` include points at. Drive with `scripts/http_request/updrive.py HOST KEY LOGFILE press:NAME wait:S`, which prints every
update state (latest version, summary). The ESP8266 Arduino client gets `Error reading manifest: -1` on roughly a
quarter of the fetches from this Python server on every build, so count checks against the base before blaming a
PR. `http_request` on the ESP8266 needs `verify_ssl: false` even for plain http URLs.
Results: https://github.com/esphome/esphome/pull/21373#issuecomment-6337544933
@@ -1,18 +0,0 @@
# modem
## PPPoS modem component with no modem attached (#6721)
A boot check on a classic ESP32 devkit with the modem UART pins left open. `templates/modem/modem-common.yaml` is
the package (`api:`, debug `loop_time` and `free` sensors, uptime, buttons for `modem.enable`, `modem.disable` and
`modem.send_at`, a 20 s alive log); `modemo.yaml` is the modem only build with `reboot_timeout: 2min` and the
component test config's `on_boot` block at `priority: -100` (earlier, `modem.send_at` runs before the modem's
`setup()` and logs `Modem DCE not ready`); `modemw.yaml` adds `wifi:` with `reboot_timeout: 0s` so the API stays
reachable. Expect `Modem not responding at 115200 baud` once per pass with each pass blocking the loop about 1 s,
the reboot at the configured timeout (`SW_CPU_RESET`), and a `modem.disable` with no modem answering to block
the loop about 37 s (`Failed to leave data mode`, then the radio off timeout) without tripping the 30 s watchdog
floor because the component feeds it in between. Drive the Wi-Fi build with
`scripts/modem/mdrive.py HOST LOGFILE list press:NAME wait:S` (plaintext API; prints sensor and text sensor
states, saves the device log). Before 8cc8df4a1f a modem only config with `api:` failed validation because a
config dependent `AUTO_LOAD` added `network` after the dependency check; an explicit `network:` block was the
workaround. Results: https://github.com/esphome/esphome/pull/6721#issuecomment-6347395159 and
https://github.com/esphome/esphome/pull/6721#issuecomment-6348520416
@@ -1,221 +0,0 @@
"""Pull the store_yaml blob off a device over the native API (plaintext or Noise) and unpack it.
usage: syrecover.py HOST KEY OUTDIR [ORIGDIR] (KEY of - means a plaintext API)
Prints the capability flag, chunk count, advertised and received sizes, encoding, the envelope's files, and
when ORIGDIR is given compares each recovered file with ORIGDIR/<path>.
Run with PYTHONPATH pointing at the esphome tree that has store_yaml (for unpack_envelope). Needs Python 3.14
(compression.zstd) or the zstandard package.
"""
import asyncio
from pathlib import Path
import sys
import time
from aioesphomeapi import api_pb2
from aioesphomeapi._frame_helper.noise import APINoiseFrameHelper
from aioesphomeapi._frame_helper.plain_text import APIPlaintextFrameHelper
from aioesphomeapi.connection import APIConnection, ConnectionParams
from aioesphomeapi.zeroconf import ZeroconfManager
from esphome.components.store_yaml import unpack_envelope
try:
from compression import zstd
except ImportError:
try:
import zstandard as zstd
except ImportError:
sys.exit("needs Python 3.14 (compression.zstd) or the zstandard package")
HOST, KEY, OUT = sys.argv[1], sys.argv[2], Path(sys.argv[3])
ORIG = Path(sys.argv[4]) if len(sys.argv) > 4 else None
HELLO, HELLO_RESP, CONNECT, CONNECT_RESP = 1, 2, 3, 4
DISCONNECT_REQ = 5
FATAL = -1 # queued by report_fatal_error so a waiting expect() stops at once
CAPS_REQ, CAPS_RESP = 149, 150
GET_YAML_REQ, GET_YAML_RESP = 161, 162
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) -> dict[int, list]:
out: dict[int, list] = {}
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]
pos += 4
else:
raise ValueError(f"wire type {wt}")
out.setdefault(fnum, []).append(v)
return out
async def main() -> None:
loop = asyncio.get_running_loop()
queue: asyncio.Queue[tuple[int, bytes]] = asyncio.Queue()
errors: list[Exception] = []
class Shim(APIConnection):
"""Only the two callbacks the frame helper uses; the real connection logic is bypassed."""
def process_packet(self, msg_type_proto, data) -> None:
queue.put_nowait((msg_type_proto, bytes(data)))
def report_fatal_error(self, err: Exception) -> None:
errors.append(err)
queue.put_nowait((FATAL, b""))
params = ConnectionParams(
addresses=[HOST],
port=6053,
password=None,
client_info="syrecover",
keepalive=15,
zeroconf_manager=ZeroconfManager(),
noise_psk=None if KEY == "-" else KEY,
expected_name=None,
expected_mac=None,
timezone=None,
provide_time=True,
)
shim = Shim(params, None, False, HOST)
if KEY == "-":
helper = APIPlaintextFrameHelper(
connection=shim, client_info="syrecover", log_name=HOST
)
else:
helper = APINoiseFrameHelper(
connection=shim,
noise_psk=KEY,
expected_name=None,
expected_mac=None,
client_info="syrecover",
log_name=HOST,
)
await loop.create_connection(lambda: helper, HOST, 6053)
try:
await asyncio.wait_for(helper.ready_future, 10)
except Exception as exc: # noqa: BLE001
sys.exit(f"handshake failed: {exc!r} {errors}")
print(f"transport {'plaintext' if KEY == '-' else 'noise'}: handshake done")
async def expect(msg_type: int, timeout: float = 10) -> bytes:
while True:
t, d = await asyncio.wait_for(queue.get(), timeout)
if t == msg_type:
return d
if t == FATAL:
sys.exit(
f"transport error while waiting for message {msg_type}: {errors}"
)
if t == DISCONNECT_REQ:
sys.exit(
f"device closed the connection while waiting for message {msg_type}"
)
print(f"skipping message type {t} while waiting for {msg_type}")
hello = api_pb2.HelloRequest(
client_info="syrecover", api_version_major=1, api_version_minor=10
)
helper.write_packets([(HELLO, hello.SerializeToString())], False)
await expect(HELLO_RESP)
# Sent like the client library does; a device without a password never answers it
helper.write_packets(
[(CONNECT, api_pb2.AuthenticationRequest().SerializeToString())], False
)
helper.write_packets(
[(CAPS_REQ, api_pb2.DeviceCapabilitiesRequest().SerializeToString())], False
)
caps = fields(await expect(CAPS_RESP))
supported = None
if 7 in caps:
supported = fields(caps[7][0]).get(1) == [1]
print(f"capabilities: store_yaml field present={7 in caps} supported={supported}")
t0 = time.monotonic()
helper.write_packets([(GET_YAML_REQ, b"")], False)
chunks: list[bytes] = []
total = encoding = None
done = False
while not done:
f = fields(await expect(GET_YAML_RESP, 20))
if 1 in f:
chunks.append(f[1][0])
done = f.get(2) == [1]
if 3 in f:
total = f[3][0]
if 4 in f:
encoding = f[4][0].decode()
dt = time.monotonic() - t0
compressed = b"".join(chunks)
print(f"chunks {len(chunks)} sizes {[len(c) for c in chunks]}")
print(
f"received {len(compressed)} bytes, advertised {total}, encoding {encoding!r}, {dt:.2f} s"
)
# a second request right after: the device must answer again, not stay stuck
helper.write_packets([(GET_YAML_REQ, b"")], False)
again = 0
done = False
while not done:
f = fields(await expect(GET_YAML_RESP, 20))
again += len(f.get(1, [b""])[0])
done = f.get(2) == [1]
print(f"second request: {again} bytes")
helper.close()
if errors:
sys.exit(f"transport errors: {errors}")
if encoding != "zstd":
sys.exit(f"unexpected encoding {encoding!r}, expected 'zstd'")
if total is not None and len(compressed) != total:
sys.exit(f"received {len(compressed)} bytes but the device advertised {total}")
if again != len(compressed):
sys.exit(
f"the second request returned {again} bytes, the first {len(compressed)}"
)
envelope = zstd.decompress(compressed)
files = unpack_envelope(envelope)
print(
f"envelope {len(envelope)} bytes, {len(files)} files, ratio {len(compressed) / len(envelope):.2f}"
)
OUT.mkdir(parents=True, exist_ok=True)
for rel, content in files.items():
target = OUT / rel.replace("../", "up_/")
if not target.resolve().is_relative_to(OUT.resolve()):
print("skipping a path outside the output directory:", rel)
continue
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
note = ""
if ORIG is not None:
orig = (ORIG / rel).resolve()
if orig.exists():
note = (
"IDENTICAL to original"
if orig.read_bytes() == content
else f"DIFFERS from original ({orig.stat().st_size} bytes)"
)
else:
note = "no original"
print(f" {rel}: {len(content)} bytes {note}")
asyncio.run(main())
@@ -1,58 +0,0 @@
"""Serve update manifests: stable.json, beta.json and slow.json (answered after a delay); logs every request."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import sys
import time
PORT = int(sys.argv[1])
DELAY = float(sys.argv[2]) if len(sys.argv) > 2 else 4.0
def manifest(name: str, version: str) -> bytes:
builds = [
{
"chipFamily": fam,
"ota": {
"path": f"/{name}.bin",
"md5": "d41d8cd98f00b204e9800998ecf8427e",
"summary": f"{name} channel",
},
}
for fam in ("ESP8266", "ESP32")
]
return json.dumps(
{"name": f"Bench {name}", "version": version, "builds": builds}
).encode()
DOCS = {
"/stable.json": manifest("stable", "1.0.0"),
"/beta.json": manifest("beta", "2.0.0-beta1"),
"/slow.json": manifest("slow", "3.0.0-slow"),
}
class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802
body = DOCS.get(self.path)
print(
f"{time.strftime('%H:%M:%S')} {self.client_address[0]} GET {self.path} -> {404 if body is None else 200}",
flush=True,
)
if body is None:
self.send_error(404)
return
if self.path == "/slow.json":
time.sleep(DELAY)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args) -> None: # quiet default log
return
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
@@ -1,129 +0,0 @@
"""Drive the http_request update harness over the API: press buttons, print every update state and save the log.
usage: updrive.py HOST KEY(- for plaintext) LOGFILE OP [OP ...]
OP: list | press:NAME | set:NAME=VALUE | sel:NAME=OPTION | wait:SECONDS
"""
import asyncio
import pathlib
import sys
import time
from aioesphomeapi import (
APIClient,
BinarySensorState,
ButtonInfo,
LogLevel,
NumberInfo,
NumberState,
SelectInfo,
SelectState,
SensorInfo,
SensorState,
TextSensorInfo,
TextSensorState,
UpdateState,
)
HOST, KEY, LOGFILE = sys.argv[1], sys.argv[2], sys.argv[3]
OPS = sys.argv[4:]
T0 = time.time()
def stamp() -> str:
return f"{time.time() - T0:7.3f}"
async def main() -> None:
with pathlib.Path(LOGFILE).open("w", encoding="utf-8") as log:
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
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)}
def on_state(s) -> None:
e = by_key.get(s.key)
if e is None:
return
if isinstance(s, TextSensorState):
print(
f"{stamp()} TXT {e.name} = {s.state!r}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, UpdateState):
print(
f"{stamp()} UPD {e.name} latest={s.latest_version!r} title={s.title!r} summary={s.release_summary!r} in_progress={s.in_progress}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, BinarySensorState):
print(
f"{stamp()} BIN {e.name} = {s.state}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, NumberState):
print(
f"{stamp()} NUM {e.name} = {s.state:.0f}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, SelectState):
print(
f"{stamp()} SEL {e.name} = {s.state!r}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, SensorState) and isinstance(e, SensorInfo):
print(f"{stamp()} SEN {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("=")
numbers = {e.name: e for e in ents if isinstance(e, NumberInfo)}
if name not in numbers:
sys.exit(f"no number named {name!r}")
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(2.0)
await asyncio.sleep(1)
await c.disconnect()
asyncio.run(main())
@@ -1,86 +0,0 @@
"""Drive the modem test build over the API: press buttons, print sensor states, save the device log.
usage: mdrive.py HOST LOGFILE OP [OP ...]
OP: list | press:NAME | wait:SECONDS
"""
import asyncio
import pathlib
import sys
import time
from aioesphomeapi import (
APIClient,
ButtonInfo,
LogLevel,
SensorInfo,
SensorState,
TextSensorInfo,
TextSensorState,
)
HOST, LOGFILE = sys.argv[1], sys.argv[2]
OPS = sys.argv[3:]
T0 = time.time()
def stamp() -> str:
return f"{time.time() - T0:7.3f}"
async def main() -> None:
with pathlib.Path(LOGFILE).open("w", encoding="utf-8") as log:
c = APIClient(HOST, 6053, None)
await c.connect(login=True)
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)}
def on_state(s) -> None:
e = by_key.get(s.key)
if e is None:
return
if isinstance(s, TextSensorState):
print(
f"{stamp()} TXT {e.name} = {s.state!r}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, SensorState) and isinstance(e, SensorInfo):
print(f"{stamp()} SEN {e.name} = {s.state:.1f}", flush=True)
def on_log(msg) -> None:
log.write(f"{stamp()} {msg.message.decode(errors='replace')}\n")
log.flush()
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
await asyncio.sleep(1.5)
for op in OPS:
if op == "list":
for n in sorted(buttons):
print("button:", n)
for e in ents:
if isinstance(e, (TextSensorInfo, SensorInfo)):
print("entity:", type(e).__name__, 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)
else:
sys.exit(f"unknown op {op!r}")
await asyncio.sleep(2.0)
await asyncio.sleep(1)
await c.disconnect()
asyncio.run(main())
@@ -1,150 +0,0 @@
substitutions:
# the fake instrument's BLE address; pass it with `-s peri_mac AA:BB:CC:DD:EE:FF`
peri_mac: "00:00:00:00:00:00"
esphome:
name: midicli
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: VERBOSE
logs:
esp32_ble_tracker: DEBUG
ble_client: DEBUG
esp32_ble: DEBUG
api: DEBUG
scheduler: DEBUG
sensor: DEBUG
wifi: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
api:
reboot_timeout: 0s
esp32_ble_tracker:
ble_client:
- mac_address: ${peri_mac}
id: midi_client
ble_midi:
id: test_midi
ble_client_id: midi_client
max_sysex_size: 64
pair: false
on_connect:
- logger.log: "MIDI device connected"
- ble_midi.send_program_change:
id: test_midi
program: 0
on_disconnect:
- logger.log: "MIDI device disconnected"
on_note_on:
- logger.log:
format: "Note on %u velocity %u channel %u"
args: [note, velocity, channel]
on_note_off:
- logger.log:
format: "Note off %u channel %u"
args: [note, channel]
on_control_change:
- logger.log:
format: "CC %u = %u on channel %u"
args: [control_number, value, channel]
on_program_change:
- logger.log:
format: "Program %u on channel %u"
args: [program, channel]
on_pitch_bend:
- logger.log:
format: "Pitch bend %d on channel %u"
args: [value, channel]
on_sysex:
- lambda: |-
ESP_LOGI("midi", "SysEx of %u bytes: %s", (unsigned) data.size(), format_hex_pretty(data.data(), data.size()).c_str());
on_message:
- lambda: |-
ESP_LOGI("midi", "Raw message of %u bytes: %s", (unsigned) data.size(), format_hex_pretty(data.data(), data.size()).c_str());
sensor:
- platform: debug
free:
name: Heap Free
loop_time:
name: Loop Time
debug:
update_interval: 30s
button:
- platform: template
name: "MIDI send tests"
on_press:
- ble_midi.send_note_on:
id: test_midi
note: 60
- ble_midi.send_note_on:
id: test_midi
note: !lambda "return 60;"
velocity: !lambda "return 100;"
channel: !lambda "return 1;"
- ble_midi.send_note_off:
id: test_midi
note: 60
velocity: 0
channel: 1
- ble_midi.send_control_change:
id: test_midi
control_number: 7
value: 64
- ble_midi.send_pitch_bend:
id: test_midi
value: -8192
- ble_midi.send_sysex:
id: test_midi
payload: [0x7E, 0x7F, 0x06, 0x01]
- ble_midi.send_raw:
id: test_midi
data: !lambda "return std::vector<uint8_t>{0x90, 0x3C, 0x40};"
- if:
condition:
ble_midi.connected:
id: test_midi
then:
- logger.log: "Still connected"
else:
- logger.log: "Not connected"
- platform: template
name: "MIDI send out of range"
on_press:
- ble_midi.send_note_on:
id: test_midi
note: !lambda "return 200;"
- ble_midi.send_control_change:
id: test_midi
control_number: 1
value: 1
channel: !lambda "return 16;"
- ble_midi.send_pitch_bend:
id: test_midi
value: 8191
- ble_midi.send_sysex:
id: test_midi
payload: [0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7]
- ble_midi.send_raw:
id: test_midi
data: !lambda "return std::vector<uint8_t>(140, 0x00);"
- platform: template
name: "MIDI send long sysex"
on_press:
- ble_midi.send_sysex:
id: test_midi
payload: !lambda "return std::vector<uint8_t>(100, 0x11);"
@@ -1,177 +0,0 @@
esphome:
name: midiperi
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
logger:
hardware_uart: USB_SERIAL_JTAG
level: DEBUG
api:
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
esp32_ble_server:
id: ble_server
manufacturer: FakeMIDI
on_connect:
- lambda: ESP_LOGI("peri", "central connected %d", id);
on_disconnect:
- lambda: ESP_LOGI("peri", "central disconnected %d", id);
services:
- uuid: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700
advertise: true
characteristics:
- id: midi_io
uuid: 7772E5DB-3868-4112-A1A9-F2669D106BF3
read: true
write: true
write_no_response: true
notify: true
on_write:
- lambda: |-
ESP_LOGI("peri", "RX %u bytes: %s", (unsigned) x.size(), format_hex_pretty(x.data(), x.size()).c_str());
script:
- id: emit
parameters:
pkt: std::vector<uint8_t>
then:
- lambda: |-
ESP_LOGI("peri", "TX %u bytes: %s", (unsigned) pkt.size(), format_hex_pretty(pkt.data(), pkt.size()).c_str());
id(midi_io).set_value(std::vector<uint8_t>(pkt));
id(midi_io).notify();
button:
- platform: restart
name: P Restart
- platform: template
name: P Note On
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x90, 0x3C, 0x64};'
- platform: template
name: P Note Off
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x80, 0x3C, 0x00};'
- platform: template
name: P Control Change
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xB0, 0x07, 0x40};'
- platform: template
name: P Program Change
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xC5, 0x05};'
- platform: template
name: P Pitch Bend
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xE0, 0x7F, 0x7F};'
- platform: template
name: P Two Notes One Packet
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x90, 0x3C, 0x64, 0x81, 0x91, 0x3E, 0x50};'
- platform: template
name: P Running Status
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x90, 0x40, 0x64, 0x41, 0x64};'
- platform: template
name: P Sysex One Packet
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF0, 0x7E, 0x7F, 0x06, 0x01, 0x80, 0xF7};'
- platform: template
name: P Sysex Split
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF0, 0x41, 0x10, 0x42};'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x12, 0x00, 0x40, 0x80, 0xF7};'
- platform: template
name: P Sysex Oversize
on_press:
- script.execute:
id: emit
pkt: !lambda 'std::vector<uint8_t> v{0x80, 0x80, 0xF0}; for (int i = 0; i < 17; i++) v.push_back(i); return v;'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'std::vector<uint8_t> v{0x80}; for (int i = 0; i < 19; i++) v.push_back(0x20 + i); return v;'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'std::vector<uint8_t> v{0x80}; for (int i = 0; i < 19; i++) v.push_back(0x40 + i); return v;'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'std::vector<uint8_t> v{0x80}; for (int i = 0; i < 19; i++) v.push_back(0x60 + i); return v;'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF7};'
- platform: template
name: P Sysex Cut Short
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF0, 0x41, 0x10, 0x80, 0x90, 0x3C, 0x64};'
- platform: template
name: P Clock
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF8};'
- platform: template
name: P Aftertouch
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xD3, 0x45};'
- platform: template
name: P Orphan Data Byte
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x3C, 0x64};'
- platform: template
name: P Burst
on_press:
- repeat:
count: 50
then:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x90, 0x30, 0x40, 0x80, 0x80, 0x30, 0x00};'
- delay: 20ms
@@ -1,10 +0,0 @@
esphome:
name: upd32
esp32:
board: esp32dev
framework:
type: esp-idf
packages:
common: !include ../upd-common.yaml
@@ -1,8 +0,0 @@
esphome:
name: upd8266
esp8266:
board: d1_mini
packages:
common: !include ../upd-common.yaml
@@ -1,52 +0,0 @@
substitutions:
base: !secret manifest_base
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
api:
reboot_timeout: 0s
http_request:
verify_ssl: false
timeout: 10s
ota:
- platform: http_request
id: ota_http
update:
- platform: http_request
id: upd
name: Firmware
source: ${base}/stable.json
update_interval: 1h
button:
- platform: template
name: Check
on_press:
- lambda: id(upd).check();
- platform: template
name: URL Slow Cstr
on_press:
- lambda: |-
std::string url = "${base}/slow.json";
id(upd).set_source_url(url.c_str());
- platform: template
name: URL Stable Literal
on_press:
- lambda: id(upd).set_source_url("${base}/stable.json");
- platform: template
name: Heap
on_press:
- lambda: |-
#ifdef USE_ESP8266
ESP_LOGI("harness", "heap free %u", ESP.getFreeHeap());
#else
ESP_LOGI("harness", "heap free %u", (unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
#endif
@@ -1,95 +0,0 @@
substitutions:
base: !secret manifest_base
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
on_message:
- level: DEBUG
then:
- lambda: |-
// Inline check path: replace the URL from inside the check (ESP8266 runs the check on the main loop)
if (id(swap_in_check) && strstr(message, "Content-Length") != nullptr) {
id(swap_in_check) = false;
std::string url = "${base}/beta.json";
id(upd).set_source_url(url);
ESP_LOGI("harness", "URL replaced from inside the check");
}
api:
reboot_timeout: 0s
http_request:
verify_ssl: false
timeout: 10s
ota:
- platform: http_request
id: ota_http
update:
- platform: http_request
id: upd
name: Firmware
source: ${base}/stable.json
update_interval: 1h
globals:
- id: swap_in_check
type: bool
initial_value: "false"
button:
- platform: template
name: Check
on_press:
- lambda: id(upd).check();
- platform: template
name: URL Beta String
on_press:
- lambda: |-
std::string url = "${base}/beta.json";
id(upd).set_source_url(url);
- platform: template
name: URL Slow Cstr
on_press:
- lambda: |-
std::string url = "${base}/slow.json";
id(upd).set_source_url(url.c_str());
- platform: template
name: URL Stable Literal
on_press:
- lambda: id(upd).set_source_url("${base}/stable.json");
- platform: template
name: URL Temporary
on_press:
- lambda: id(upd).set_source_url(std::string("${base}/beta.json"));
- platform: template
name: Same URL Back
on_press:
- lambda: id(upd).set_source_url(id(upd).get_source_url());
- platform: template
name: Check Then Swap
on_press:
- lambda: |-
// ESP32: the check task reads slow.json while the URL is replaced under it
id(upd).check();
std::string url = "${base}/beta.json";
id(upd).set_source_url(url);
- platform: template
name: Arm Swap In Check
on_press:
- lambda: id(swap_in_check) = true;
- platform: template
name: Heap
on_press:
- lambda: |-
#ifdef USE_ESP8266
ESP_LOGI("harness", "heap free %u", ESP.getFreeHeap());
#else
ESP_LOGI("harness", "heap free %u", (unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
#endif
@@ -1,65 +0,0 @@
esp32:
board: esp32dev
framework:
type: esp-idf
watchdog_timeout: 30s
logger:
level: DEBUG
api:
reboot_timeout: 0s
modem:
id: atmodem
rx_pin: GPIO26
tx_pin: GPIO27
baud_rate: 115200
model: SIM7600
apn: internet
pin_code: "0000"
init_at:
- ATI
urc:
name: Modem URC
reboot_timeout: ${modem_reboot_timeout}
on_connect:
logger.log: "modem connected"
on_disconnect:
logger.log: "modem disconnected"
on_sync:
logger.log: "modem synced"
sensor:
- platform: uptime
name: Uptime
update_interval: 30s
- platform: debug
free:
name: Heap Free
loop_time:
name: Loop Time
debug:
update_interval: 30s
button:
- platform: template
name: Modem Enable
on_press:
- modem.enable
- platform: template
name: Modem Disable
on_press:
- modem.disable
- platform: template
name: Modem Send AT
on_press:
- modem.send_at: "AT+CGSN"
interval:
- interval: 20s
then:
- lambda: |-
ESP_LOGI("alive", "uptime %u s enabled=%d connected=%d", (unsigned) (millis() / 1000),
id(atmodem).is_enabled(), id(atmodem).is_connected());
@@ -1,26 +0,0 @@
substitutions:
modem_reboot_timeout: 2min
esphome:
name: modemo
on_boot:
priority: -100
then:
- modem.disable
- modem.enable
- if:
condition:
modem.enabled:
then:
modem.send_at: "AT+CGSN"
else:
modem.enable:
- if:
condition:
not:
modem.connected:
then:
logger.log: modem not connected at boot
packages:
common: !include modem-common.yaml
@@ -1,12 +0,0 @@
substitutions:
modem_reboot_timeout: 0s
esphome:
name: modemw
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
packages:
common: !include modem-common.yaml
@@ -1,15 +0,0 @@
esphome:
name: sy8266
esp8266:
board: d1_mini
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
packages:
common: !include ../sy-common.yaml
store_yaml:
@@ -1,16 +0,0 @@
esphome:
name: sy8266s
esp8266:
board: d1_mini
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
packages:
common: !include ../sy-common.yaml
store_yaml:
include_secrets: true
@@ -1,24 +0,0 @@
# shared package for the store_yaml hardware test
substitutions:
greeting: "hello from the package"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
ota:
- platform: esphome
password: recoverme123
sensor:
- platform: template
name: Package Sensor
lambda: return 42.0;
update_interval: 60s
packages:
extra: !include sy-extra.yaml
@@ -1,6 +0,0 @@
# second level include
text_sensor:
- platform: template
name: Greeting
lambda: return std::string("${greeting}");
update_interval: 60s
+1 -1
View File
@@ -286,7 +286,7 @@ jobs:
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Run prek
uses: j178/prek-action@44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d # v3.1.0
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
+1 -1
View File
@@ -94,7 +94,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
skip-existing: true
+6 -12
View File
@@ -209,17 +209,17 @@ file does, and it is the authority when they disagree. The most useful starting
registered with `automation.ApplyBytes` (see Actions below), which keeps a constant payload in flash;
`TEMPLATABLE_BYTES` is for a `play()` that does more with the bytes.
* **Fixed sets of names** (status words, model names): a `PROGMEM_STRING_TABLE` (`esphome/core/progmem.h`),
logged with `get_log_str` and `LOG_STR_ARG`, published on a text sensor with
`publish_state(Table::get_progmem_str(idx, fallback))`, which copies it out of flash on ESP8266.
logged with `get_log_str` and `LOG_STR_ARG`, copied out with `ESPHOME_strncpy_P` before `publish_state`.
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `xor_bytes`, `sum16_bytes`,
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`, `assign_bits`); pick
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
`if (!this->status_comm_result(ok)) return;` sets or clears the warning. A child sensor that skips repeated
values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
end of `setup()` when the entities need a state at boot.
@@ -650,13 +650,7 @@ file does, and it is the authority when they disagree. The most useful starting
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
* **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`.
* **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`. On the ESP32, Arduino is
built as a component of ESP-IDF, so an ESP-IDF API, sdkconfig option or version floor is there on both
frameworks: gate on `esp32.require_idf_version(cv.Version(5, 5, 3), "psram_dma")`, which checks the
platform and the ESP-IDF under either framework, as `spi`, `usb_host` and `wifi` do. Reserve
`cv.only_with_framework("esp-idf")` for a build that has been tried on Arduino and fails there, and write
the actual failure next to it (`sendspin`: arduino-esp32 below ESP-IDF 6.0 bundles a second libsodium,
and from 6.0 sendspin-cpp's enumerators collide with `Arduino.h` macros such as `DISPLAY`).
* **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`, `cv.only_with_esp_idf`.
* **Schema Extensions:**
```python
CONFIG_SCHEMA = cv.Schema({ ... })
+1 -12
View File
@@ -158,7 +158,6 @@ esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @esphome/core
esphome/components/delonghi/* @grob6000
esphome/components/delta_wallbox/* @epicRE
esphome/components/dew_point/* @CFlix
esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
@@ -176,11 +175,6 @@ esphome/components/ds2484/* @mrk-its
esphome/components/ds248x/* @tomwellnitz
esphome/components/ds3231/* @linkedupbits
esphome/components/dsmr/* @glmnet @PolarGoose
esphome/components/duco/* @kokx
esphome/components/duco/number/* @kokx
esphome/components/duco/select/* @kokx
esphome/components/duco/sensor/* @kokx
esphome/components/duco/text_sensor/* @kokx
esphome/components/duty_time/* @dudanov
esphome/components/ecocomfort2/* @gledian
esphome/components/ee895/* @Stock-M
@@ -380,8 +374,7 @@ esphome/components/m5stack_8angle/* @rnauber
esphome/components/mapping/* @clydebarrow
esphome/components/matrix_keypad/* @ssieb
esphome/components/max17043/* @blacknell
esphome/components/max17048/* @B48D81EFCC @ryansch
esphome/components/max170xx/* @ryansch
esphome/components/max17048/* @B48D81EFCC
esphome/components/max31865/* @DAVe3283
esphome/components/max31888/* @JiriPrchal
esphome/components/max44009/* @berfenger
@@ -436,7 +429,6 @@ esphome/components/modbus_controller/sensor/* @martgras
esphome/components/modbus_controller/switch/* @martgras
esphome/components/modbus_controller/text_sensor/* @martgras
esphome/components/modbus_server/* @exciton
esphome/components/modem/* @oarcher
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
@@ -470,8 +462,6 @@ esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_interface/* @clydebarrow
esphome/components/packet_interface/packet_transport/* @clydebarrow
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
@@ -692,7 +682,6 @@ esphome/components/tuya/water_heater/* @iago-veiga
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_interface/* @clydebarrow
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/uart_mux/* @kbx81
esphome/components/uart_tcp/* @Bascht74
-2
View File
@@ -84,5 +84,3 @@ FROM base-${BUILD_TYPE} AS final
# Copy esphome and install
COPY . /esphome
RUN uv pip install --no-cache-dir -e /esphome
# The tree has no .git; lets SBOM manifests describe it as the release it was built from
ENV ESPHOME_IS_DOCKER_IMAGE=true
+2
View File
@@ -44,6 +44,7 @@ from esphome.platformio.library import (
normalize_dependencies,
parse_library_json,
parse_library_properties,
warn_properties_depends,
)
_LOGGER = logging.getLogger(__name__)
@@ -282,6 +283,7 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
"resolved automatically; add them with add_library() if needed",
name,
)
warn_properties_depends(name, data)
build = data.get("build")
if isinstance(build, dict) and build.get("extraScript"):
# Scripts only run on the converted path; building without
-1
View File
@@ -49,7 +49,6 @@ from esphome.cpp_generator import ( # noqa: F401
static_const_array,
static_function,
templatable,
unique_global_id,
variable,
with_local_variable,
)
@@ -91,19 +91,17 @@ def alarm_control_panel_schema(
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
}
)
ALARM_CONTROL_PANEL_CODE_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
cv.Optional(CONF_CODE): cv.templatable(cv.string),
}
)
ALARM_CONTROL_PANEL_CONDITION_SCHEMA = ALARM_CONTROL_PANEL_ACTION_SCHEMA
ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
}
)
_CALLBACK_AUTOMATIONS = (
@@ -196,7 +194,7 @@ for _name, _arm in (
):
automation.register_apply_action(
_name,
ALARM_CONTROL_PANEL_CODE_ACTION_SCHEMA,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
automation.ApplyCall(_arm),
automation.ApplyField(CONF_CODE, "set_code", cg.std_string),
call="make_call",
@@ -293,9 +293,14 @@ void BekenSPILEDStripLightOutput::set_led_params(uint8_t bit0, uint8_t bit1, uin
}
void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
// protect from refreshing too often
uint32_t now = micros();
if (this->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
if (this->max_refresh_rate_.has_value() && *this->max_refresh_rate_ != 0 &&
(now - this->last_refresh_) < *this->max_refresh_rate_) {
// try again next loop iteration, so that this change won't get lost
this->schedule_show();
return;
}
this->last_refresh_ = now;
this->mark_shown_();
@@ -364,6 +369,17 @@ void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
this->status_clear_warning();
}
light::ESPColorView BekenSPILEDStripLightOutput::get_view_internal(int32_t index) const {
const light::ChannelColors &colors = this->channel_colors_;
uint8_t *led = this->buf_ + (index * colors.bytes_per_led());
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
&this->effect_data_[index],
&this->correction_};
}
void BekenSPILEDStripLightOutput::dump_config() {
ESP_LOGCONFIG(TAG,
"Beken SPI LED Strip:\n"
@@ -374,7 +390,7 @@ void BekenSPILEDStripLightOutput::dump_config() {
" Channel colors: %s\n"
" Max refresh rate: %" PRIu32 "\n"
" Number of LEDs: %u",
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_, this->num_leds_);
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_);
}
float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -18,7 +18,15 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
float get_setup_priority() const override;
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->channel_colors_.has_white()) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
}
return traits;
}
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
@@ -30,16 +38,14 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
void set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency);
void clear_effect_data() override {
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->buf_, this->effect_data_, index, &this->correction_);
}
light::ESPColorView get_view_internal(int32_t index) const override;
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
@@ -56,7 +62,7 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
uint32_t max_refresh_rate_{0};
optional<uint32_t> max_refresh_rate_{};
};
} // namespace esphome::beken_spi_led_strip
+13 -11
View File
@@ -3,6 +3,7 @@ from logging import getLogger
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server, zigbee
from esphome.components.const import CONF_ON_STATE_CHANGE
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
@@ -28,6 +29,7 @@ from esphome.const import (
CONF_STATE,
CONF_TIMING,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BATTERY_CHARGING,
DEVICE_CLASS_CARBON_MONOXIDE,
@@ -59,12 +61,10 @@ from esphome.const import (
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
lazy_load_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -433,14 +433,14 @@ def validate_publish_initial_state(value):
_BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
.extend(entity_helpers.ZIGBEE_BINARY_SENSOR_SCHEMA)
.extend(zigbee.BINARY_SENSOR_SCHEMA)
.extend(
{
cv.GenerateID(): cv.declare_id(BinarySensor),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTBinarySensorComponent")
mqtt.MQTTBinarySensorComponent
),
cv.Exclusive(
CONF_PUBLISH_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
@@ -508,7 +508,7 @@ _BINARY_SENSOR_SCHEMA = (
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
_BINARY_SENSOR_SCHEMA.add_extra(lazy_load_validator("zigbee", "validate_binary_sensor"))
_BINARY_SENSOR_SCHEMA.add_extra(zigbee.validate_binary_sensor)
def binary_sensor_schema(
@@ -619,12 +619,14 @@ async def setup_binary_sensor_core_(var, config):
CORE.add_job(_build_binary_sensor_automations, var, config)
await entity_helpers.setup_entity_integrations(var, config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if "zigbee" in CORE.loaded_integrations:
from esphome.components import zigbee
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
await zigbee.setup_binary_sensor(var, config)
await zigbee.setup_binary_sensor(var, config)
async def register_binary_sensor(var, config):
-5
View File
@@ -20,11 +20,6 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
platform="bp1658cj", hub_key=CONF_BP1658CJ_ID
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
-5
View File
@@ -21,11 +21,6 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
platform="bp5758d", hub_key=CONF_BP5758D_ID
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
+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_();
}
+2 -3
View File
@@ -24,9 +24,7 @@ climate::ClimateTraits ClimateIR::traits() {
return traits;
}
void ClimateIR::setup() { this->setup_(this->restore_state_()); }
void ClimateIR::setup_(optional<climate::ClimateDeviceRestoreState> restore) {
void ClimateIR::setup() {
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
@@ -45,6 +43,7 @@ void ClimateIR::setup_(optional<climate::ClimateDeviceRestoreState> restore) {
}
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->apply(this);
} else {
@@ -60,10 +60,6 @@ class ClimateIR : public Component,
/// Transmit via IR the state of this climate controller.
virtual void transmit_state() = 0;
/// setup() with the restored state already loaded, for subclasses that read it first. Load it only once:
/// every restore_state_() call takes a new preference slot on some platforms.
void setup_(optional<climate::ClimateDeviceRestoreState> restore);
// Dummy implement on_receive so implementation is optional for inheritors
bool on_receive(remote_base::RemoteReceiveData data) override { return false; };
-5
View File
@@ -21,11 +21,6 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
)
FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
platform="dac7678", hub_key=CONF_DAC7678_ID
)
async def to_code(config: ConfigType) -> MockObj:
paren = await cg.get_variable(config[CONF_DAC7678_ID])
var = cg.new_Pvariable(config[CONF_ID])
@@ -1,42 +0,0 @@
import esphome.codegen as cg
from esphome.components import ble_client, time
import esphome.config_validation as cv
from esphome.const import CONF_PASSWORD, CONF_PIN, CONF_TIME_ID
from esphome.types import ConfigType
CODEOWNERS = ["@epicRE"]
DEPENDENCIES = ["ble_client"]
DOMAIN = "delta_wallbox"
MULTI_CONF = True
CONF_DELTA_WALLBOX_ID = "delta_wallbox_id"
CONF_USER = "user"
delta_wallbox_ns = cg.esphome_ns.namespace("delta_wallbox")
DeltaWallbox = delta_wallbox_ns.class_(
"DeltaWallbox", ble_client.BLEClientNode, cg.PollingComponent
)
CONFIG_SCHEMA = (
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DeltaWallbox),
cv.Required(CONF_PASSWORD): cv.All(cv.string_strict, cv.Length(max=22)),
cv.Optional(CONF_PIN): cv.int_range(min=0, max=999999),
cv.Optional(CONF_USER, default="admin"): cv.All(
cv.string_strict, cv.Length(max=6)
),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
.extend(ble_client.BLE_CLIENT_SCHEMA)
.extend(cv.polling_component_schema("15s"))
)
async def to_code(config: ConfigType) -> None:
var = await ble_client.new_ble_node_component(
config, config[CONF_USER], config[CONF_PASSWORD]
)
cg.add_optional(var.set_pin, config.get(CONF_PIN))
await cg.add_variable(var.set_time_id, config.get(CONF_TIME_ID))
@@ -1,30 +0,0 @@
from esphome.components import button
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import CONF_DELTA_WALLBOX_ID, DeltaWallbox
DEPENDENCIES = ["delta_wallbox"]
CONF_START_CHARGING = "start_charging"
CONF_STOP_CHARGING = "stop_charging"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DELTA_WALLBOX_ID): cv.use_id(DeltaWallbox),
cv.Optional(CONF_START_CHARGING): button.button_schema(
button.ApplyButton,
icon="mdi:play-circle-outline",
),
cv.Optional(CONF_STOP_CHARGING): button.button_schema(
button.ApplyButton,
icon="mdi:stop-circle-outline",
),
}
)
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_DELTA_WALLBOX_ID])
await buttons(CONF_START_CHARGING, None, apply="start_charging()")
await buttons(CONF_STOP_CHARGING, None, apply="stop_charging()")
@@ -1,691 +0,0 @@
#include "delta_wallbox.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#ifdef USE_ESP32
namespace esphome::delta_wallbox {
ESPHOME_LOG_TAG(TAG, "delta_wallbox");
// Modbus function codes
static constexpr uint8_t FC_READ_HOLDING_REGISTERS = 0x03;
static constexpr uint8_t FC_WRITE_SINGLE_REGISTER = 0x06;
static constexpr uint8_t FC_WRITE_MULTIPLE_REGISTERS = 0x10;
static constexpr uint8_t FC_EXCEPTION_FLAG = 0x80;
// Response lengths in bytes, CRC included
static constexpr size_t READ_RESPONSE_OVERHEAD = 5;
static constexpr size_t WRITE_RESPONSE_LEN = 8;
static constexpr size_t EXCEPTION_RESPONSE_LEN = 5;
// Frame bodies without CRC
static constexpr size_t LOGIN_BODY_LEN = 35;
static constexpr size_t LOGIN_PASS_LEN = 22;
static constexpr size_t LOGIN_USER_LEN = 6;
static constexpr size_t TIME_SYNC_BODY_LEN = 15;
static constexpr uint8_t REGISTER_BYTE_COUNT = 2;
static constexpr uint8_t TWO_REGISTER_BYTE_COUNT = 4;
static constexpr uint8_t CHARGING_INFO_BYTE_COUNT = 18;
static constexpr uint8_t SERIAL_TEXT_BYTE_COUNT = 14;
static constexpr uint32_t RETRANSMIT_TIMEOUT_MS = 4000;
static constexpr uint32_t RETRANSMIT_TIMEOUT_ID = 0;
static constexpr uint8_t MAX_PAIRING_ATTEMPTS = 5;
static constexpr uint16_t PAIRING_STATUS_READY = 0x0007;
static constexpr uint16_t PAIRING_STATUS_WAITING = 0x0008;
static constexpr uint8_t LOGIN_ACCEPTED = 0x01;
static constexpr uint8_t CMD_POLL_CHARGING[] = {0x01, 0x03, 0x01, 0x1B, 0x00, 0x09};
static constexpr uint8_t CMD_GET_MODEL[] = {0x01, 0x03, 0x01, 0x85, 0x00, 0x0E};
static constexpr uint8_t CMD_GET_BT_FW_VERSION[] = {0x02, 0x03, 0x01, 0x00, 0x00, 0x01};
static constexpr uint8_t CMD_CHECK_PAIRING[] = {0x02, 0x03, 0x01, 0x00, 0x00, 0x03};
static constexpr uint8_t CMD_GET_LOGIN_USER[] = {0x01, 0x03, 0x01, 0x7D, 0x00, 0x03};
static constexpr uint8_t CMD_READ_LOGIN_STATUS[] = {0x01, 0x03, 0x01, 0x80, 0x00, 0x01};
static constexpr uint8_t CMD_GET_LOGIN_ROLE[] = {0x01, 0x03, 0x01, 0x81, 0x00, 0x01};
static constexpr uint8_t CMD_GET_CHARGER_FW_VERSION[] = {0x01, 0x03, 0x01, 0x10, 0x00, 0x02};
static constexpr uint8_t CMD_GET_MAX_CURRENT[] = {0x01, 0x03, 0x01, 0x12, 0x00, 0x02};
static constexpr uint8_t CMD_GET_SERIAL_NUMBER[] = {0x01, 0x03, 0x01, 0x09, 0x00, 0x01};
static constexpr uint8_t CMD_GET_ERROR_CODE[] = {0x01, 0x03, 0x01, 0xB5, 0x00, 0x01};
static constexpr uint8_t CMD_START_CHARGING[] = {0x01, 0x06, 0x01, 0x02, 0x00, 0x01};
static constexpr uint8_t CMD_STOP_CHARGING[] = {0x01, 0x06, 0x01, 0x03, 0x00, 0x01};
// Indexed by DeltaState
PROGMEM_STRING_TABLE(DeltaStateStrings, "CHECK_PAIRING", "CONNECTED", "GET_CHARGER_FW_VERSION", "GET_BT_FW_VERSION",
"GET_LOGIN_RESULT", "GET_LOGIN_RESULT0", "GET_LOGIN_ROLE", "GET_LOGIN_USER", "GET_MODEL",
"GET_SERIAL_NUMBER", "HALTED", "IDLE", "SEND_LOGIN_CREDENTIALS", "TIME_SYNC");
static_assert(DeltaStateStrings::LAST_INDEX == static_cast<uint8_t>(DeltaState::DELTA_STATE_TIME_SYNC),
"DeltaStateStrings must list every DeltaState");
static const LogString *delta_state_to_string(DeltaState state) {
return DeltaStateStrings::get_log_str(static_cast<uint8_t>(state), DeltaStateStrings::LAST_INDEX);
}
// The function code a handshake step waits for; 0 where any frame is handled
static uint8_t expected_function_code(DeltaState state) {
switch (state) {
case DeltaState::DELTA_STATE_IDLE:
case DeltaState::DELTA_STATE_HALTED:
case DeltaState::DELTA_STATE_CONNECTED:
return 0;
case DeltaState::DELTA_STATE_SEND_LOGIN_CREDENTIALS:
case DeltaState::DELTA_STATE_TIME_SYNC:
return FC_WRITE_MULTIPLE_REGISTERS;
default:
return FC_READ_HOLDING_REGISTERS;
}
}
// UUID references
static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID =
esp32_ble_tracker::ESPBTUUID::from_raw("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
static const esp32_ble_tracker::ESPBTUUID TX_UUID =
esp32_ble_tracker::ESPBTUUID::from_raw("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
static const esp32_ble_tracker::ESPBTUUID RX_UUID =
esp32_ble_tracker::ESPBTUUID::from_raw("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
// Appends the little-endian Modbus CRC-16 to the len bytes in frame and returns the new length.
static size_t append_crc(uint8_t *frame, size_t len) {
const uint16_t crc = crc16(frame, static_cast<uint16_t>(len));
frame[len] = static_cast<uint8_t>(crc & 0xFF);
frame[len + 1] = static_cast<uint8_t>(crc >> 8);
return len + 2;
}
static bool verify_crc(const uint8_t *data, size_t len) {
if (len < EXCEPTION_RESPONSE_LEN)
return false;
const uint16_t calculated = crc16(data, static_cast<uint16_t>(len - 2));
const uint16_t received = data[len - 2] | (data[len - 1] << 8);
return calculated == received;
}
// Login write body: 22-byte password and 6-byte user, both left-padded with zeros
static std::array<uint8_t, LOGIN_BODY_LEN> build_login_body(StringRef user, StringRef password) {
std::array<uint8_t, LOGIN_BODY_LEN> frame = {
0x01, 0x10, 0x01, 0x72, 0x00, 0x0E, 0x1C, // Unit, FC16, register 0x0172, 14 registers, 28 bytes
};
const size_t pass_len = std::min(password.size(), LOGIN_PASS_LEN);
std::copy_n(password.c_str(), pass_len, frame.begin() + 7 + (LOGIN_PASS_LEN - pass_len));
uint8_t *user_chunk = frame.data() + 7 + LOGIN_PASS_LEN;
bool is_digit = !user.empty();
for (size_t i = 0; i < user.size() && is_digit; i++) {
is_digit = isdigit(static_cast<unsigned char>(user.c_str()[i]));
}
if (is_digit) {
const uint64_t val = strtoull(user.c_str(), nullptr, 10);
for (size_t i = 0; i < LOGIN_USER_LEN; i++) {
user_chunk[LOGIN_USER_LEN - 1 - i] = static_cast<uint8_t>((val >> (8 * i)) & 0xFF);
}
} else {
const size_t user_len = std::min(user.size(), LOGIN_USER_LEN);
std::copy_n(user.c_str(), user_len, user_chunk + LOGIN_USER_LEN - user_len);
}
return frame;
}
static std::array<uint8_t, TIME_SYNC_BODY_LEN> build_time_sync_body(int year, int month, int day, int hour, int minute,
int second, int weekday) {
const uint8_t weekday_bitmap = static_cast<uint8_t>(1 << weekday); // Sunday=bit0, Mon=bit1...
return {
0x01,
0x10,
0x01,
0x14,
0x00,
0x04,
0x08, // Unit, FC16, register 0x0114, 4 registers, 8 bytes
static_cast<uint8_t>(year % 100),
static_cast<uint8_t>(month),
weekday_bitmap,
static_cast<uint8_t>(day),
static_cast<uint8_t>(hour),
static_cast<uint8_t>(minute),
static_cast<uint8_t>(second),
0x00,
};
}
void DeltaWallbox::dump_config() {
ESP_LOGCONFIG(TAG, "Delta Wallbox:");
ESP_LOGCONFIG(TAG, " User: %s", this->user_.c_str());
ESP_LOGCONFIG(TAG, " BLE PIN configured: %s", this->has_pin_ ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"));
LOG_UPDATE_INTERVAL(this);
}
void DeltaWallbox::update() {
if (this->is_connected()) {
// Start poll flow sequence
ESP_LOGD(TAG, "Polling charging info...");
this->send_frame_(CMD_POLL_CHARGING, sizeof(CMD_POLL_CHARGING));
}
}
void DeltaWallbox::write_frame_(const uint8_t *frame, size_t len) {
if (this->tx_char_handle_ == 0) {
ESP_LOGW(TAG, "TX characteristic handle is not ready while in state %s. Dropping %u-byte frame.",
LOG_STR_ARG(delta_state_to_string(this->state_)), (unsigned int) len);
return;
}
ESP_LOGD(TAG, "Sending %u-byte frame in state %s (conn_id=%u, tx_handle=0x%04X).", (unsigned int) len,
LOG_STR_ARG(delta_state_to_string(this->state_)), this->parent_->get_conn_id(), this->tx_char_handle_);
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->tx_char_handle_, len,
const_cast<uint8_t *>(frame), ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status != ESP_GATT_OK) {
ESP_LOGW(TAG, "esp_ble_gattc_write_char failed, status=%d", status);
}
}
// Copies body into a stack buffer, appends the CRC and writes the frame.
void DeltaWallbox::send_frame_(const uint8_t *body, size_t len) {
std::array<uint8_t, MAX_FRAME_LEN> frame{};
std::copy_n(body, len, frame.begin());
this->write_frame_(frame.data(), append_crc(frame.data(), len));
}
void DeltaWallbox::drop_rx_bytes_(size_t count) {
count = std::min(count, this->rx_len_);
std::memmove(this->rx_buffer_.data(), this->rx_buffer_.data() + count, this->rx_len_ - count);
this->rx_len_ -= count;
}
void DeltaWallbox::send_next_state_command_() {
ESP_LOGD(TAG, "Preparing command for state %s.", LOG_STR_ARG(delta_state_to_string(this->state_)));
switch (this->state_) {
case DeltaState::DELTA_STATE_GET_MODEL:
ESP_LOGI(TAG, "Requesting charger model name...");
this->send_frame_(CMD_GET_MODEL, sizeof(CMD_GET_MODEL));
break;
case DeltaState::DELTA_STATE_GET_BT_FW_VERSION:
ESP_LOGI(TAG, "Requesting BT firmware version...");
this->send_frame_(CMD_GET_BT_FW_VERSION, sizeof(CMD_GET_BT_FW_VERSION));
break;
case DeltaState::DELTA_STATE_CHECK_PAIRING:
ESP_LOGI(TAG, "Checking pairing status...");
this->send_frame_(CMD_CHECK_PAIRING, sizeof(CMD_CHECK_PAIRING));
break;
case DeltaState::DELTA_STATE_GET_LOGIN_USER:
ESP_LOGI(TAG, "Querying user profile...");
this->send_frame_(CMD_GET_LOGIN_USER, sizeof(CMD_GET_LOGIN_USER));
break;
case DeltaState::DELTA_STATE_GET_LOGIN_RESULT0:
ESP_LOGI(TAG, "Checking pre-login status register...");
this->send_frame_(CMD_READ_LOGIN_STATUS, sizeof(CMD_READ_LOGIN_STATUS));
break;
case DeltaState::DELTA_STATE_SEND_LOGIN_CREDENTIALS: {
ESP_LOGI(TAG, "Transmitting login credentials...");
const auto body = build_login_body(this->user_, this->password_);
this->send_frame_(body.data(), body.size());
break;
}
case DeltaState::DELTA_STATE_GET_LOGIN_RESULT:
ESP_LOGI(TAG, "Verifying login result...");
this->send_frame_(CMD_READ_LOGIN_STATUS, sizeof(CMD_READ_LOGIN_STATUS));
break;
case DeltaState::DELTA_STATE_GET_LOGIN_ROLE:
ESP_LOGI(TAG, "Checking role privileges...");
this->send_frame_(CMD_GET_LOGIN_ROLE, sizeof(CMD_GET_LOGIN_ROLE));
break;
case DeltaState::DELTA_STATE_TIME_SYNC: {
#ifdef USE_TIME
// Only a valid time is written; the charger keeps its own clock otherwise
if (this->time_id_ != nullptr) {
auto tm = this->time_id_->now();
if (tm.is_valid()) {
ESP_LOGI(TAG, "Synchronizing wallbox clock...");
// ESPTime day_of_week is 1 (Sunday) to 7 (Saturday); the charger counts from 0
const auto body = build_time_sync_body(tm.year, tm.month, tm.day_of_month, tm.hour, tm.minute, tm.second,
tm.day_of_week - 1);
this->send_frame_(body.data(), body.size());
break;
}
}
#endif
this->advance_to_(DeltaState::DELTA_STATE_GET_CHARGER_FW_VERSION);
break;
}
case DeltaState::DELTA_STATE_GET_CHARGER_FW_VERSION:
ESP_LOGI(TAG, "Requesting charger main firmware version...");
this->send_frame_(CMD_GET_CHARGER_FW_VERSION, sizeof(CMD_GET_CHARGER_FW_VERSION));
break;
case DeltaState::DELTA_STATE_GET_SERIAL_NUMBER:
ESP_LOGI(TAG, "Requesting wallbox serial number...");
this->send_frame_(CMD_GET_SERIAL_NUMBER, sizeof(CMD_GET_SERIAL_NUMBER));
break;
default:
this->cancel_timeout(RETRANSMIT_TIMEOUT_ID);
return;
}
this->set_timeout(RETRANSMIT_TIMEOUT_ID, RETRANSMIT_TIMEOUT_MS, [this]() { this->retransmit_(); });
}
void DeltaWallbox::retransmit_() {
ESP_LOGW(TAG, "No answer in login state %s within %u ms. Sending the command again...",
LOG_STR_ARG(delta_state_to_string(this->state_)), (unsigned int) RETRANSMIT_TIMEOUT_MS);
this->send_next_state_command_();
}
void DeltaWallbox::advance_to_(DeltaState next) {
ESP_LOGD(TAG, "State transition: %s -> %s", LOG_STR_ARG(delta_state_to_string(this->state_)),
LOG_STR_ARG(delta_state_to_string(next)));
this->state_ = next;
this->send_next_state_command_();
}
void DeltaWallbox::handle_frame_(const uint8_t *frame, size_t len) {
ESP_LOGD(TAG, "Received valid frame in state %s: func=0x%02X len=%u",
LOG_STR_ARG(delta_state_to_string(this->state_)), frame[1], (unsigned int) len);
if (frame[1] & FC_EXCEPTION_FLAG) {
ESP_LOGE(TAG, "Modbus exception received in state %s! Error code: 0x%02X",
LOG_STR_ARG(delta_state_to_string(this->state_)), frame[2]);
if (this->state_ == DeltaState::DELTA_STATE_TIME_SYNC) {
// The clock write is optional, so a charger that refuses it is used without it
ESP_LOGW(TAG, "Charger refused the clock sync; continuing without it");
this->advance_to_(DeltaState::DELTA_STATE_GET_CHARGER_FW_VERSION);
} else if (expected_function_code(this->state_) != 0) {
// The charger refused a handshake step; sending the same request again would not change that
this->halt_(LOG_STR("Modbus exception during login"));
}
return;
}
const uint8_t expected = expected_function_code(this->state_);
if (expected != 0 && frame[1] != expected) {
// A late answer to a retransmitted request; the pending one is answered or sent again by the timer
ESP_LOGD(TAG, "Ignoring func=0x%02X reply while waiting for 0x%02X", frame[1], expected);
return;
}
switch (this->state_) {
case DeltaState::DELTA_STATE_IDLE:
break; // No command is pending; ignore the frame.
case DeltaState::DELTA_STATE_GET_MODEL: {
char model[MAX_FRAME_LEN];
const size_t data_len = std::min<size_t>(frame[2], len - READ_RESPONSE_OVERHEAD);
size_t model_len = 0;
for (size_t i = 0; i < data_len; i++) {
if (frame[3 + i] != '\0')
model[model_len++] = static_cast<char>(frame[3 + i]);
}
model[model_len] = '\0';
ESP_LOGI(TAG, "Discovered Wallbox Model Name: %s", model);
if (this->model_name_text_sensor_ != nullptr) {
this->model_name_text_sensor_->publish_state(model);
}
this->advance_to_(DeltaState::DELTA_STATE_GET_BT_FW_VERSION);
break;
}
case DeltaState::DELTA_STATE_GET_BT_FW_VERSION: {
if (frame[2] >= REGISTER_BYTE_COUNT) {
uint8_t major = frame[3] % 16;
uint8_t minor = frame[4];
char ver[16];
snprintf(ver, sizeof(ver), "%d.%d", major, minor);
ESP_LOGI(TAG, "Discovered Wallbox BT FW: %s", ver);
if (this->bt_fw_version_text_sensor_ != nullptr) {
this->bt_fw_version_text_sensor_->publish_state(ver);
}
}
this->advance_to_(DeltaState::DELTA_STATE_CHECK_PAIRING);
break;
}
case DeltaState::DELTA_STATE_CHECK_PAIRING: {
if (frame[2] >= REGISTER_BYTE_COUNT) {
uint16_t status = (frame[3] << 8) | frame[4];
ESP_LOGI(TAG, "Pairing Status Register: 0x%04X", status);
if (status == PAIRING_STATUS_READY) {
this->pairing_in_progress_ = false;
this->pairing_attempts_ = 0;
ESP_LOGI(TAG, "Pairing ready and confirmed!");
this->advance_to_(DeltaState::DELTA_STATE_GET_LOGIN_USER);
} else if (status == PAIRING_STATUS_WAITING) {
ESP_LOGW(TAG, "Wallbox is waiting to be paired by the system controller (attempt %u, in_progress=%s).",
this->pairing_attempts_ + 1,
this->pairing_in_progress_ ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"));
this->pairing_attempts_++;
if (!this->pairing_in_progress_) {
ESP_LOGI(TAG, "Initiating BLE pairing with wallbox.");
auto err = this->parent_->pair();
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to initiate BLE pairing, err=%d", err);
} else {
this->pairing_in_progress_ = true;
}
}
} else {
ESP_LOGW(TAG, "Unexpected pairing status 0x%04X", status);
this->pairing_attempts_++;
}
if (this->pairing_attempts_ >= MAX_PAIRING_ATTEMPTS) {
this->halt_(LOG_STR("Too many failed/waiting pairing checks"));
}
} else {
ESP_LOGW(TAG, "Pairing status response too short: byte_count=%u frame_len=%u", frame[2], (unsigned int) len);
}
break;
}
case DeltaState::DELTA_STATE_GET_LOGIN_USER: {
this->advance_to_(DeltaState::DELTA_STATE_GET_LOGIN_RESULT0);
break;
}
case DeltaState::DELTA_STATE_GET_LOGIN_RESULT0: {
if (frame[2] == REGISTER_BYTE_COUNT) {
this->advance_to_(DeltaState::DELTA_STATE_SEND_LOGIN_CREDENTIALS);
}
break;
}
case DeltaState::DELTA_STATE_SEND_LOGIN_CREDENTIALS: {
this->advance_to_(DeltaState::DELTA_STATE_GET_LOGIN_RESULT);
break;
}
case DeltaState::DELTA_STATE_GET_LOGIN_RESULT: {
// Only the one-register status answer counts; a late reply to another read has a different length
if (frame[2] == REGISTER_BYTE_COUNT) {
uint8_t login_val = frame[4]; // respondent data_lo
if (login_val == LOGIN_ACCEPTED) {
ESP_LOGI(TAG, "Credentials accepted! Logged in as %s.", this->user_.c_str());
this->advance_to_(DeltaState::DELTA_STATE_GET_LOGIN_ROLE);
} else {
ESP_LOGE(TAG, "Login validation failed (0x%02X). Check password.", login_val);
this->halt_(LOG_STR("Bad credentials"));
}
}
break;
}
case DeltaState::DELTA_STATE_HALTED: {
break; // Authentication is halted; ignore any late replies.
}
case DeltaState::DELTA_STATE_GET_LOGIN_ROLE: {
if (frame[2] == REGISTER_BYTE_COUNT) {
this->advance_to_(DeltaState::DELTA_STATE_TIME_SYNC);
}
break;
}
case DeltaState::DELTA_STATE_TIME_SYNC: {
this->advance_to_(DeltaState::DELTA_STATE_GET_CHARGER_FW_VERSION);
break;
}
case DeltaState::DELTA_STATE_GET_CHARGER_FW_VERSION: {
if (frame[2] >= TWO_REGISTER_BYTE_COUNT) {
char ver[16];
snprintf(ver, sizeof(ver), "%d.%d", frame[3], frame[4]);
ESP_LOGI(TAG, "Discovered Charger Main FW: %s", ver);
if (this->fw_version_text_sensor_ != nullptr) {
this->fw_version_text_sensor_->publish_state(ver);
}
}
this->advance_to_(DeltaState::DELTA_STATE_GET_SERIAL_NUMBER);
break;
}
case DeltaState::DELTA_STATE_GET_SERIAL_NUMBER: {
// Serial read (0x0109) returns 14 ASCII bytes instead of one register
if (frame[2] >= SERIAL_TEXT_BYTE_COUNT) {
char serial[SERIAL_TEXT_BYTE_COUNT + 1];
size_t serial_len = 0;
for (size_t i = 0; i < SERIAL_TEXT_BYTE_COUNT; i++) {
if (frame[3 + i] != '\0')
serial[serial_len++] = static_cast<char>(frame[3 + i]);
}
serial[serial_len] = '\0';
ESP_LOGI(TAG, "Wallbox serial text: %s", serial);
if (this->serial_number_text_sensor_ != nullptr)
this->serial_number_text_sensor_->publish_state(serial);
}
this->advance_to_(DeltaState::DELTA_STATE_CONNECTED);
ESP_LOGI(TAG, "Authentication complete. Core parameters loaded.");
this->status_clear_warning();
this->update(); // perform first poll update immediately
break;
}
case DeltaState::DELTA_STATE_CONNECTED: {
// Connected polling responses
if (frame[1] == FC_READ_HOLDING_REGISTERS) {
uint8_t byte_count = frame[2];
if (byte_count == CHARGING_INFO_BYTE_COUNT) {
// Parsing 0x011B charging info (9 registers)
uint32_t power_raw = (frame[3] << 24) | (frame[4] << 16) | (frame[5] << 8) | frame[6];
uint32_t energy_raw = (frame[7] << 24) | (frame[8] << 16) | (frame[9] << 8) | frame[10];
uint8_t minutes = frame[15];
uint8_t hours = frame[16];
uint8_t state_byte = frame[17];
float power_kw = power_raw / 1000.0f;
float energy_kwh = energy_raw / 1000.0f;
float charging_minutes = hours * 60 + minutes;
if (this->power_sensor_ != nullptr)
this->power_sensor_->publish_state(power_kw);
if (this->energy_sensor_ != nullptr)
this->energy_sensor_->publish_state(energy_kwh);
if (this->charging_time_sensor_ != nullptr)
this->charging_time_sensor_->publish_state(charging_minutes);
if (this->charger_state_sensor_ != nullptr)
this->charger_state_sensor_->publish_state(state_byte);
// Retrieve maximum current output setting next
this->send_frame_(CMD_GET_MAX_CURRENT, sizeof(CMD_GET_MAX_CURRENT));
} else if (byte_count == TWO_REGISTER_BYTE_COUNT) {
// Parsing 0x0112 max output setting (2 registers)
uint32_t current_raw = (frame[3] << 24) | (frame[4] << 16) | (frame[5] << 8) | frame[6];
float amps = current_raw / 10.0f;
if (this->max_current_sensor_ != nullptr) {
this->max_current_sensor_->publish_state(amps);
}
// Poll the error code register every cycle
this->send_frame_(CMD_GET_ERROR_CODE, sizeof(CMD_GET_ERROR_CODE));
} else if (byte_count == REGISTER_BYTE_COUNT) {
// Error status register (0x01B5)
uint16_t reg_val = (frame[3] << 8) | frame[4];
char hex_str[16];
snprintf(hex_str, sizeof(hex_str), "%04X", reg_val);
if (this->error_code_text_sensor_ != nullptr)
this->error_code_text_sensor_->publish_state(hex_str);
}
} else if (frame[1] == FC_WRITE_SINGLE_REGISTER) {
// Confirmation frame of Start/Stop write actions
ESP_LOGI(TAG, "Wallbox charging command successfully acknowledged.");
}
break;
}
}
}
void DeltaWallbox::reset_state_() {
ESP_LOGW(TAG, "Resetting component state from %s. pairing_attempts=%u, pairing_in_progress=%s, rx_buffer=%u bytes",
LOG_STR_ARG(delta_state_to_string(this->state_)), this->pairing_attempts_,
this->pairing_in_progress_ ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"), (unsigned int) this->rx_len_);
this->cancel_timeout(RETRANSMIT_TIMEOUT_ID);
this->rx_len_ = 0;
this->state_ = DeltaState::DELTA_STATE_IDLE;
this->pairing_attempts_ = 0;
this->pairing_in_progress_ = false;
this->status_set_warning();
}
void DeltaWallbox::halt_(const LogString *reason) {
ESP_LOGE(TAG, "Halting authentication: %s. A reconnect is required to retry.", LOG_STR_ARG(reason));
this->cancel_timeout(RETRANSMIT_TIMEOUT_ID);
this->rx_len_ = 0;
this->state_ = DeltaState::DELTA_STATE_HALTED;
this->pairing_attempts_ = 0;
this->pairing_in_progress_ = false;
this->status_set_warning(reason);
}
void DeltaWallbox::set_charging_state(bool state) {
if (!this->is_connected()) {
ESP_LOGW(TAG, "Cannot send charging command while wallbox is not fully connected.");
return;
}
if (state) {
ESP_LOGI(TAG, "Sending remote START charging command...");
this->send_frame_(CMD_START_CHARGING, sizeof(CMD_START_CHARGING));
} else {
ESP_LOGI(TAG, "Sending remote STOP charging command...");
this->send_frame_(CMD_STOP_CHARGING, sizeof(CMD_STOP_CHARGING));
}
}
void DeltaWallbox::start_charging() { this->set_charging_state(true); }
void DeltaWallbox::stop_charging() { this->set_charging_state(false); }
void DeltaWallbox::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected from Bluetooth wallbox client (conn_id=%u, reason=%d). Resetting state.",
param->disconnect.conn_id, param->disconnect.reason);
this->reset_state_();
this->node_state = esp32_ble_tracker::ClientState::IDLE;
break;
}
case ESP_GATTC_OPEN_EVT: {
ESP_LOGI(TAG, "GATT open event received: status=%d conn_id=%u", param->open.status, param->open.conn_id);
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
ESP_LOGI(TAG, "GATT service discovery completed: status=%d", param->search_cmpl.status);
auto *tx_char = this->parent_->get_characteristic(SERVICE_UUID, TX_UUID);
auto *rx_char = this->parent_->get_characteristic(SERVICE_UUID, RX_UUID);
if (tx_char == nullptr || rx_char == nullptr) {
ESP_LOGE(TAG, "Could not discover matching NUS TX/RX service on this BLE device.");
this->reset_state_();
break;
}
this->tx_char_handle_ = tx_char->handle;
this->rx_char_handle_ = rx_char->handle;
esp_err_t err = this->parent_->register_for_notify(this->rx_char_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "register_for_notify failed, status=%d", err);
}
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
ESP_LOGI(TAG, "Register-for-notify event: status=%d handle=0x%04X", param->reg_for_notify.status,
param->reg_for_notify.handle);
if (param->reg_for_notify.handle == this->rx_char_handle_) {
ESP_LOGI(TAG, "Subscribed to RX notification handle 0x%04X successfully.", this->rx_char_handle_);
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
if (this->state_ == DeltaState::DELTA_STATE_IDLE) {
ESP_LOGI(TAG, "GATT connection established. Starting authentication state machine.");
this->advance_to_(DeltaState::DELTA_STATE_GET_MODEL);
}
}
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.handle != this->rx_char_handle_)
break;
const size_t value_len = param->notify.value_len;
ESP_LOGD(TAG, "Notification received on handle 0x%04X with %u bytes. Buffer before append=%u bytes.",
param->notify.handle, param->notify.value_len, (unsigned int) this->rx_len_);
if (value_len > MAX_FRAME_LEN - this->rx_len_) {
ESP_LOGW(TAG, "Notification overflows the %u-byte RX buffer. Discarding buffered data.",
(unsigned int) MAX_FRAME_LEN);
this->rx_len_ = 0;
if (value_len > MAX_FRAME_LEN)
break;
}
std::memcpy(this->rx_buffer_.data() + this->rx_len_, param->notify.value, value_len);
this->rx_len_ += value_len;
bool progress = true;
while (progress && this->rx_len_ >= 3) {
progress = false;
uint8_t func_code = this->rx_buffer_[1];
size_t expected_len = 0;
if (func_code == FC_READ_HOLDING_REGISTERS) {
expected_len = this->rx_buffer_[2] + READ_RESPONSE_OVERHEAD;
} else if (func_code == FC_WRITE_MULTIPLE_REGISTERS || func_code == FC_WRITE_SINGLE_REGISTER) {
expected_len = WRITE_RESPONSE_LEN;
} else if (func_code == (FC_EXCEPTION_FLAG | FC_READ_HOLDING_REGISTERS) ||
func_code == (FC_EXCEPTION_FLAG | FC_WRITE_MULTIPLE_REGISTERS) ||
func_code == (FC_EXCEPTION_FLAG | FC_WRITE_SINGLE_REGISTER)) {
expected_len = EXCEPTION_RESPONSE_LEN;
}
if (expected_len > 0) {
if (this->rx_len_ >= expected_len) {
if (verify_crc(this->rx_buffer_.data(), expected_len)) {
// The frame stays in rx_buffer_ during handle_frame_(); it is dropped afterwards.
this->handle_frame_(this->rx_buffer_.data(), expected_len);
this->drop_rx_bytes_(expected_len);
progress = true;
} else {
ESP_LOGW(TAG, "CRC mismatch while parsing notification data in state %s. Dropping one byte from buffer.",
LOG_STR_ARG(delta_state_to_string(this->state_)));
// Shift by one byte to realign with the next possible frame start
this->drop_rx_bytes_(1);
progress = true;
}
}
} else {
ESP_LOGW(TAG,
"Unsupported function code 0x%02X while parsing notification data in state %s. Advancing buffer.",
func_code, LOG_STR_ARG(delta_state_to_string(this->state_)));
this->drop_rx_bytes_(1);
progress = true;
}
}
break;
}
default:
break;
}
}
void DeltaWallbox::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
case ESP_GAP_BLE_PASSKEY_REQ_EVT: {
if (!this->parent_->check_addr(param->ble_security.ble_req.bd_addr))
return;
if (!this->has_pin_) {
ESP_LOGW(TAG, "Wallbox requested a BLE passkey, but no pin is configured. Set pin: under delta_wallbox:.");
return;
}
ESP_LOGI(TAG, "Wallbox requested a BLE passkey. Replying with configured YAML pin.");
esp_err_t err = esp_ble_passkey_reply(param->ble_security.ble_req.bd_addr, true, this->pin_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to send BLE passkey reply: %s", esp_err_to_name(err));
}
break;
}
case ESP_GAP_BLE_AUTH_CMPL_EVT:
if (!this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr))
return;
this->pairing_in_progress_ = false;
if (param->ble_security.auth_cmpl.success) {
ESP_LOGI(TAG,
"BLE pairing completed successfully. auth_mode=%d addr_type=%d. Re-checking wallbox pairing status.",
param->ble_security.auth_cmpl.auth_mode, param->ble_security.auth_cmpl.addr_type);
this->pairing_attempts_ = 0;
if (this->node_state == esp32_ble_tracker::ClientState::ESTABLISHED &&
this->state_ == DeltaState::DELTA_STATE_CHECK_PAIRING) {
this->advance_to_(DeltaState::DELTA_STATE_CHECK_PAIRING);
}
} else {
ESP_LOGW(TAG, "BLE pairing failed, reason=%d", param->ble_security.auth_cmpl.fail_reason);
}
break;
default:
break;
}
}
} // namespace esphome::delta_wallbox
#endif
@@ -1,103 +0,0 @@
#pragma once
#ifdef USE_ESP32
#include <array>
#include "esphome/core/component.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
namespace esphome::delta_wallbox {
// Largest FC03 response: unit, function, byte count, 255 data bytes, CRC.
static constexpr size_t MAX_FRAME_LEN = 260;
enum class DeltaState : uint8_t {
DELTA_STATE_CHECK_PAIRING,
DELTA_STATE_CONNECTED,
DELTA_STATE_GET_CHARGER_FW_VERSION,
DELTA_STATE_GET_BT_FW_VERSION,
DELTA_STATE_GET_LOGIN_RESULT,
DELTA_STATE_GET_LOGIN_RESULT0,
DELTA_STATE_GET_LOGIN_ROLE,
DELTA_STATE_GET_LOGIN_USER,
DELTA_STATE_GET_MODEL,
DELTA_STATE_GET_SERIAL_NUMBER,
DELTA_STATE_HALTED,
DELTA_STATE_IDLE,
DELTA_STATE_SEND_LOGIN_CREDENTIALS,
DELTA_STATE_TIME_SYNC,
};
class DeltaWallbox : public esphome::ble_client::BLEClientNode, public PollingComponent {
public:
DeltaWallbox(const char *user, const char *password) : user_(user), password_(password) {}
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
void dump_config() override;
void set_pin(uint32_t pin) {
this->pin_ = pin;
this->has_pin_ = true;
}
#ifdef USE_TIME
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
#endif
SUB_SENSOR(power)
SUB_SENSOR(energy)
SUB_SENSOR(charging_time)
SUB_SENSOR(charger_state)
SUB_SENSOR(max_current)
SUB_TEXT_SENSOR(serial_number)
SUB_TEXT_SENSOR(fw_version)
SUB_TEXT_SENSOR(bt_fw_version)
SUB_TEXT_SENSOR(model_name)
SUB_TEXT_SENSOR(error_code)
void set_charging_state(bool state);
void start_charging();
void stop_charging();
bool is_connected() {
return this->node_state == esp32_ble_tracker::ClientState::ESTABLISHED &&
this->state_ == DeltaState::DELTA_STATE_CONNECTED;
}
protected:
void write_frame_(const uint8_t *frame, size_t len);
void send_frame_(const uint8_t *body, size_t len);
void send_next_state_command_();
void retransmit_();
void advance_to_(DeltaState next);
void handle_frame_(const uint8_t *frame, size_t len);
void drop_rx_bytes_(size_t count);
void reset_state_();
// Stops the handshake until the next BLE disconnect; used for failures a retry can't fix.
void halt_(const LogString *reason);
// Members ordered by size so nothing is padded
StringRef user_;
StringRef password_;
uint32_t pin_{0};
#ifdef USE_TIME
time::RealTimeClock *time_id_{nullptr};
#endif
size_t rx_len_{0};
uint16_t rx_char_handle_{0};
uint16_t tx_char_handle_{0};
DeltaState state_{DeltaState::DELTA_STATE_IDLE};
uint8_t pairing_attempts_{0};
bool has_pin_{false};
bool pairing_in_progress_{false};
std::array<uint8_t, MAX_FRAME_LEN> rx_buffer_{};
};
} // namespace esphome::delta_wallbox
#endif
@@ -1,66 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ENERGY,
CONF_MAX_CURRENT,
CONF_POWER,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_MINUTE,
)
from esphome.types import ConfigType
from . import CONF_DELTA_WALLBOX_ID, DeltaWallbox
DEPENDENCIES = ["delta_wallbox"]
CONF_CHARGING_TIME = "charging_time"
CONF_CHARGER_STATE = "charger_state"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DELTA_WALLBOX_ID): cv.use_id(DeltaWallbox),
cv.Optional(CONF_CHARGER_STATE): sensor.sensor_schema(
accuracy_decimals=0,
),
cv.Optional(CONF_MAX_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=1,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_CHARGING_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MINUTE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DELTA_WALLBOX_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_CHARGER_STATE, hub.set_charger_state_sensor)
await sensors(CONF_MAX_CURRENT, hub.set_max_current_sensor)
await sensors(CONF_POWER, hub.set_power_sensor)
await sensors(CONF_ENERGY, hub.set_energy_sensor)
await sensors(CONF_CHARGING_TIME, hub.set_charging_time_sensor)
@@ -1,34 +0,0 @@
import esphome.codegen as cg
from esphome.components import text_sensor
from esphome.components.const import CONF_ERROR_CODE, CONF_SERIAL_NUMBER
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import CONF_DELTA_WALLBOX_ID, DeltaWallbox
DEPENDENCIES = ["delta_wallbox"]
CONF_FW_VERSION = "fw_version"
CONF_BT_FW_VERSION = "bt_fw_version"
CONF_MODEL_NAME = "model_name"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DELTA_WALLBOX_ID): cv.use_id(DeltaWallbox),
cv.Optional(CONF_SERIAL_NUMBER): text_sensor.text_sensor_schema(),
cv.Optional(CONF_FW_VERSION): text_sensor.text_sensor_schema(),
cv.Optional(CONF_BT_FW_VERSION): text_sensor.text_sensor_schema(),
cv.Optional(CONF_MODEL_NAME): text_sensor.text_sensor_schema(),
cv.Optional(CONF_ERROR_CODE): text_sensor.text_sensor_schema(),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DELTA_WALLBOX_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_SERIAL_NUMBER, hub.set_serial_number_text_sensor)
await text_sensors(CONF_FW_VERSION, hub.set_fw_version_text_sensor)
await text_sensors(CONF_BT_FW_VERSION, hub.set_bt_fw_version_text_sensor)
await text_sensors(CONF_MODEL_NAME, hub.set_model_name_text_sensor)
await text_sensors(CONF_ERROR_CODE, hub.set_error_code_text_sensor)
@@ -1,10 +1,7 @@
#include "dfrobot_sen0465_uart.h"
#include <algorithm>
namespace esphome::dfrobot_sen0465_uart {
using dfrobot_sen0465_base::FRAME_HEAD;
using dfrobot_sen0465_base::FRAME_SIZE;
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
@@ -18,30 +15,21 @@ void DFRobotSEN0465UARTComponent::setup() {
}
void DFRobotSEN0465UARTComponent::loop() {
// The tail of a reading the probe sent on its own may precede the answer; start at the next frame head.
uint8_t byte;
while (this->rx_count_ == 0 && this->peek_byte(&byte) && byte != FRAME_HEAD)
this->read_byte(&byte);
const size_t available = this->available();
if (available == 0)
return;
const size_t len = std::min<size_t>(available, FRAME_SIZE - this->rx_count_);
if (!this->read_array(this->rx_buffer_ + this->rx_count_, len))
return;
this->rx_count_ += len;
if (this->rx_count_ < FRAME_SIZE)
// The tail of a reading the probe sent on its own may precede the answer; the reader skips to the frame head.
const uint8_t *frame = nullptr;
this->read_available<FRAME_SIZE>([&](uint8_t c) { return (frame = this->reader_.feed(c)) == nullptr; });
if (frame == nullptr)
return;
this->rx_count_ = 0;
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
this->disable_loop();
this->handle_response_(this->rx_buffer_);
this->handle_response_(frame);
}
bool DFRobotSEN0465UARTComponent::write_frame(const uint8_t *frame) {
// Drop anything left over, such as the answer to the mode change or readings sent before passive mode was set
this->discard_available();
this->rx_count_ = 0;
this->reader_.reset();
this->write_array(frame, FRAME_SIZE);
return true;
}
@@ -50,7 +38,7 @@ void DFRobotSEN0465UARTComponent::read_response() {
this->enable_loop();
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
this->disable_loop();
this->rx_count_ = 0;
this->reader_.reset();
this->handle_no_response_();
});
}
@@ -2,6 +2,7 @@
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/frame_reader.h"
namespace esphome::dfrobot_sen0465_uart {
@@ -17,8 +18,7 @@ class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSE
bool write_frame(const uint8_t *frame) override;
void read_response() override;
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
uint8_t rx_count_{0};
FrameReader<dfrobot_sen0465_base::FRAME_SIZE, dfrobot_sen0465_base::FRAME_HEAD> reader_;
};
} // namespace esphome::dfrobot_sen0465_uart
+15 -36
View File
@@ -1,7 +1,5 @@
#include "ds1603l.h"
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -10,35 +8,28 @@ namespace esphome::ds1603l {
ESPHOME_LOG_TAG(TAG, "ds1603l.sensor");
void DS1603L::loop() {
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
uint8_t byte;
while (this->available() > 0 && this->read_byte(&byte)) {
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
continue;
this->read_available([this](uint8_t byte) {
const uint8_t *frame = this->reader_.feed(byte);
if (frame == nullptr) {
if (this->reader_.length() == 0) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
}
return;
}
this->rx_buffer_[this->rx_count_++] = byte;
if (this->rx_count_ < FRAME_SIZE) {
continue;
}
if (this->parse_data_()) {
this->rx_count_ = 0;
} else {
if (!this->parse_data_(frame)) {
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
this->resync_();
this->reader_.resync();
}
}
});
}
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
bool DS1603L::parse_data_() {
uint8_t header = this->rx_buffer_[0];
uint8_t data_h = this->rx_buffer_[1];
uint8_t data_l = this->rx_buffer_[2];
uint8_t checksum = this->rx_buffer_[3];
bool DS1603L::parse_data_(const uint8_t *frame) {
uint8_t header = frame[0];
uint8_t data_h = frame[1];
uint8_t data_l = frame[2];
uint8_t checksum = frame[3];
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
@@ -53,16 +44,4 @@ bool DS1603L::parse_data_() {
return true;
}
void DS1603L::resync_() {
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
size_t start = 1;
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
start++;
}
this->rx_count_ -= start;
if (this->rx_count_ > 0) {
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
}
}
} // namespace esphome::ds1603l
+4 -6
View File
@@ -6,6 +6,7 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/frame_reader.h"
namespace esphome::ds1603l {
@@ -18,13 +19,10 @@ class DS1603L final : public sensor::Sensor, public Component, public uart::UART
static constexpr uint8_t HEADER_BYTE = 0xFF;
static constexpr size_t FRAME_SIZE = 4;
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
bool parse_data_();
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
void resync_();
// Validates the checksum of the frame and publishes it. Returns false if the frame is invalid.
bool parse_data_(const uint8_t *frame);
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
FrameReader<FRAME_SIZE, HEADER_BYTE> reader_;
};
} // namespace esphome::ds1603l
-87
View File
@@ -1,87 +0,0 @@
import esphome.codegen as cg
from esphome.components import time, uart
import esphome.config_validation as cv
from esphome.const import CONF_DISCOVERY, CONF_ID, CONF_TIME, CONF_TIME_ID
from esphome.core import ID
from esphome.types import ConfigType
CODEOWNERS = ["@kokx"]
DEPENDENCIES = ["uart"]
DOMAIN = "duco"
MULTI_CONF = True
CONF_DISABLE_CRC = "disable_crc"
CONF_DUCO_ID = "duco_id"
duco_ns = cg.esphome_ns.namespace("duco")
Duco = duco_ns.class_("Duco", cg.Component, uart.UARTDevice)
# Base of every class that sends requests through the hub
DucoDevice = duco_ns.class_(
"DucoDevice", cg.PollingComponent, cg.Parented.template(Duco)
)
DucoDiscovery = duco_ns.class_("DucoDiscovery", DucoDevice)
DucoTime = duco_ns.class_("DucoTime", cg.PollingComponent, cg.Parented.template(Duco))
# Entities of the platforms take the hub with this
DUCO_COMPONENT_SCHEMA = cv.Schema({cv.GenerateID(CONF_DUCO_ID): cv.use_id(Duco)})
DISCOVERY_SCHEMA = cv.Schema({cv.GenerateID(): cv.declare_id(DucoDiscovery)}).extend(
cv.COMPONENT_SCHEMA
)
TIME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(DucoTime),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
).extend(cv.polling_component_schema("15min"))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Duco),
cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean,
cv.Optional(CONF_DISCOVERY): DISCOVERY_SCHEMA,
cv.Optional(CONF_TIME): TIME_SCHEMA,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
DOMAIN,
baud_rate=57600,
require_tx=True,
require_rx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
_request_device_slot = cg.slot_counter("DUCO_DEVICE_COUNT")
async def register_duco_device(var: cg.MockObj, config: ConfigType, hub_id: ID) -> None:
"""Register a device that sends requests, and give it an open request slot on its hub."""
await cg.register_component(var, config)
hub = await cg.get_variable(hub_id)
await cg.register_parented(var, hub)
_request_device_slot(str(hub_id))
cg.add(hub.register_device(var))
async def to_code(config: ConfigType) -> None:
var = await uart.new_uart_component(config)
cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC]))
if (discovery := config.get(CONF_DISCOVERY)) is not None:
discovery_var = cg.new_Pvariable(discovery[CONF_ID])
await register_duco_device(discovery_var, discovery, config[CONF_ID])
if (time_config := config.get(CONF_TIME)) is not None:
clock = await cg.get_variable(time_config[CONF_TIME_ID])
time_var = cg.new_Pvariable(time_config[CONF_ID], clock)
await cg.register_component(time_var, time_config)
await cg.register_parented(time_var, var)
-273
View File
@@ -1,273 +0,0 @@
#include "duco.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::duco {
ESPHOME_LOG_TAG(TAG, "duco");
static constexpr uint8_t HEADER_START = 0xAA;
static constexpr uint8_t HEADER_END = 0x55;
static constexpr uint8_t STUFFING = 0x01; // follows an 0xAA inside a message, not part of it
static constexpr uint32_t FRAME_GAP_MS = 50;
static constexpr uint32_t DISCOVERY_START_ID = 0;
static constexpr uint32_t DISCOVERY_STEP_ID = 1;
static constexpr uint32_t DISCOVERY_START_MS = 16000;
static constexpr uint32_t DISCOVERY_STEP_MS = 1600;
static constexpr uint8_t DISCOVERY_MAX_ATTEMPTS = 3;
void Duco::loop() {
const uint32_t now = App.get_loop_component_start_time();
// Read first: after a slow loop pass the rest of a message is already waiting, which is not silence
this->read_available([this, now](uint8_t byte) {
if (this->parse_byte_(byte)) {
this->last_byte_ = now;
} else {
this->reset_frame_();
}
});
// The last message of a burst has no header after it, so it ends on silence
if (this->rx_size_ != 0 && now - this->last_byte_ > FRAME_GAP_MS) {
this->finalize_message_();
this->reset_frame_();
}
}
void Duco::reset_frame_() {
this->rx_size_ = 0;
this->escaped_ = false;
}
void Duco::close_request(DucoDevice *device) {
#ifdef DUCO_DEVICE_COUNT
for (auto &pending : this->pending_) {
if (pending.device == device) {
pending.function = 0;
pending.missed = false;
this->update_warning_();
return;
}
}
#endif
}
bool Duco::parse_byte_(uint8_t byte) {
const size_t at = this->rx_size_;
if (at >= MAX_FRAME_SIZE) {
ESP_LOGV(TAG, "Message longer than %u bytes dropped", static_cast<unsigned>(MAX_FRAME_SIZE));
return false;
}
const uint8_t prev = at > 0 ? this->rx_buffer_[at - 1] : 0;
if (at == 1 && prev == HEADER_START && byte == HEADER_END)
return false; // header of the next message
if (at >= 2) {
const bool escaped = this->escaped_;
this->escaped_ = false;
if (prev == HEADER_START && !escaped) {
if (byte == STUFFING) {
this->escaped_ = true;
return true;
}
if (at >= 3 && byte == HEADER_END) {
this->rx_size_--; // drop the 0xAA of the header
this->finalize_message_();
return false;
}
}
}
this->rx_buffer_[this->rx_size_++] = byte;
return true;
}
void Duco::finalize_message_() {
const uint8_t *raw = this->rx_buffer_.data();
// length (function, id and data), function, id, data, CRC low, CRC high
if (this->rx_size_ < 5 || raw[0] < 2 || this->rx_size_ < raw[0] + 3u) {
ESP_LOGV(TAG, "Malformed message dropped: %u bytes, length byte %u", this->rx_size_, raw[0]);
return;
}
const uint8_t length = raw[0];
const uint16_t computed_crc = crc16(raw, length + 1);
const uint16_t remote_crc = encode_uint16(raw[length + 2], raw[length + 1]);
if (computed_crc != remote_crc) {
if (!this->disable_crc_) {
ESP_LOGV(TAG, "CRC check failed: %04X != %04X", computed_crc, remote_crc);
return;
}
ESP_LOGV(TAG, "CRC check failed, ignored: %04X != %04X", computed_crc, remote_crc);
}
#ifdef DUCO_DEVICE_COUNT
// The box first acknowledges a request with function + 1, then answers with function + 2
const DucoMessage message{raw[2], raw + 3, static_cast<uint8_t>(length - 2)};
for (auto &pending : this->pending_) {
if (pending.function != 0 && pending.id == message.id && pending.function + 2 == raw[1]) {
pending.function = 0;
pending.missed = false;
this->update_warning_();
pending.device->receive_response(message);
return;
}
}
// Acknowledgements (odd function codes) are expected and never match
if ((raw[1] & 1) == 0) {
ESP_LOGV(TAG, "Answer %02X with id %u matches no open request", raw[1], message.id);
}
#endif
}
#ifdef DUCO_DEVICE_COUNT
void Duco::update_warning_() {
for (const auto &pending : this->pending_) {
if (pending.missed) {
this->status_set_warning(LOG_STR("No answer from the box"));
return;
}
}
this->status_clear_warning();
}
#endif
void Duco::dump_config() {
ESP_LOGCONFIG(TAG,
"Duco:\n"
" CRC Disabled: %s",
YESNO(this->disable_crc_));
}
uint8_t Duco::next_id_() {
do {
this->last_id_++;
} while (this->last_id_ == HEADER_START || this->last_id_ == HEADER_END);
return this->last_id_;
}
void Duco::send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data) {
if (data.size() > MAX_REQUEST_DATA) {
ESP_LOGE(TAG, "Request of %u bytes is too long", static_cast<unsigned>(data.size()));
return;
}
// Not byte stuffed, as in the author's original and no capture shows a request with 0xAA. An 0xAA followed by any
// byte but 0x01 or 0x55 reads the same with or without unstuffing on the box, so only those rare pairs are
// unverified.
const size_t size = data.size();
const uint8_t id = this->next_id_();
uint8_t frame[5 + MAX_REQUEST_DATA + 2] = {HEADER_START, HEADER_END, static_cast<uint8_t>(size + 2), function, id};
std::copy_n(data.begin(), size, frame + 5);
const uint16_t crc = crc16(frame + 2, size + 3);
frame[size + 5] = crc & 0xFF;
frame[size + 6] = crc >> 8;
this->write_array(frame, size + 7);
this->flush();
#ifdef DUCO_DEVICE_COUNT
for (auto &pending : this->pending_) {
if (pending.device == device) {
pending.missed = pending.function != 0;
if (pending.missed) {
ESP_LOGV(TAG, "No answer to request %02X with id %u", pending.function, pending.id);
this->update_warning_();
}
pending.id = id;
pending.function = function;
break;
}
}
#endif
ESP_LOGV_HEX(TAG, frame + 2, size + 5, sizeof(frame), "Sent %s");
}
static const LogString *node_type_name(uint8_t type) {
switch (type) {
case 8:
return LOG_STR("UCBAT");
case 9:
return LOG_STR("UC");
case 10:
return LOG_STR("UCRH");
case 12:
return LOG_STR("UCCO2");
case 13:
return LOG_STR("VLV");
case 17:
return LOG_STR("BOX");
case 18:
return LOG_STR("SWITCH");
default:
return LOG_STR("UNKNOWN");
}
}
void DucoDiscovery::setup() {
// Give the box time to start before walking its node addresses
this->set_timeout(DISCOVERY_START_ID, DISCOVERY_START_MS, [this]() {
this->set_interval(DISCOVERY_STEP_ID, DISCOVERY_STEP_MS, [this]() { this->update(); });
});
}
void DucoDiscovery::update() {
if (this->attempts_ == DISCOVERY_MAX_ATTEMPTS) {
ESP_LOGV(TAG, "No answer for node %u", this->next_node_);
this->next_node_++;
this->attempts_ = 0;
}
if (this->next_node_ >= NODE_COUNT) {
this->cancel_interval(DISCOVERY_STEP_ID);
this->parent_->close_request(this);
return;
}
// Without an answer by the next step the same node is asked again; a retry is not a missed answer
if (this->attempts_ > 0)
this->parent_->close_request(this);
ESP_LOGV(TAG, "Discovering node %u", this->next_node_);
this->attempts_++;
this->parent_->send(this, FUNCTION_NODE, {0x01, this->next_node_});
}
void DucoDiscovery::receive_response(const DucoMessage &message) {
// Only the latest attempt is open, so a late answer to an earlier one never arrives here
if (message.size >= 1 && this->next_node_ < NODE_COUNT) {
if (message.data[0] != 0) {
ESP_LOGV(TAG, "Found node %u: type %u", this->next_node_, message.data[0]);
}
this->node_types_[this->next_node_++] = message.data[0];
this->attempts_ = 0;
}
}
void DucoDiscovery::dump_config() {
ESP_LOGCONFIG(TAG, "Discovered nodes:");
for (uint8_t node = 0; node < NODE_COUNT; node++) {
const uint8_t type = this->node_types_[node];
if (type != 0) {
ESP_LOGCONFIG(TAG, " Node %u: type %u (%s)", node, type, LOG_STR_ARG(node_type_name(type)));
}
}
if (this->next_node_ < NODE_COUNT) {
ESP_LOGCONFIG(TAG, " Still discovering, at node %u", this->next_node_);
}
}
#ifdef USE_TIME
void DucoTime::update() {
const ESPTime now = this->clock_->now();
if (!now.is_valid()) {
ESP_LOGV(TAG, "Time is not valid yet");
return;
}
const uint32_t ts = now.timestamp;
this->parent_->send(nullptr, FUNCTION_PARAMETER,
{0x05, static_cast<uint8_t>(ts), static_cast<uint8_t>(ts >> 8), static_cast<uint8_t>(ts >> 16),
static_cast<uint8_t>(ts >> 24), 0x00, 0x00, 0x00, 0x00});
}
#endif
} // namespace esphome::duco
-113
View File
@@ -1,113 +0,0 @@
#pragma once
#include <array>
#include <initializer_list>
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
namespace esphome::duco {
// Function codes of the requests; the box answers a request with function + 2
static constexpr uint8_t FUNCTION_NODE = 0x0c; // discovery, status and mode of a node
static constexpr uint8_t FUNCTION_NODE_DATA = 0x10; // a block of a node's data (sensors, serial number)
static constexpr uint8_t FUNCTION_PARAMETER = 0x24; // read or write a box parameter, set the clock
/// A received message; data points into the receive buffer and is valid only during the callback.
struct DucoMessage {
uint8_t id;
const uint8_t *data;
uint8_t size;
};
class DucoDevice;
/// The open request of one device: the answer to it is function + 2 with the same message id.
struct DucoPendingRequest {
DucoDevice *device;
uint8_t id;
uint8_t function; // 0 while nothing is open
bool missed; // the request before this one got no answer; sits in padding
};
class Duco : public uart::UARTDevice, public Component {
public:
void loop() override;
void dump_config() override;
void set_disable_crc(bool disable_crc) { this->disable_crc_ = disable_crc; }
#ifdef DUCO_DEVICE_COUNT
void register_device(DucoDevice *device) { this->pending_.push_back({device, 0, 0, false}); }
#endif
/// Forget device's open request, for a device that will not send again.
void close_request(DucoDevice *device);
/// Send a request; its answer goes to device, or nowhere if nullptr. A device's new request replaces its
/// open one, so an answer is never lost to another device's traffic.
void send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data);
protected:
// The longest answer to a request sent here is 25 bytes (node status), the longest request 9
static constexpr size_t MAX_FRAME_SIZE = 32;
static constexpr size_t MAX_REQUEST_DATA = 9;
bool parse_byte_(uint8_t byte);
void reset_frame_();
#ifdef DUCO_DEVICE_COUNT
void update_warning_(); // warn while any device's previous request went unanswered
#endif
void finalize_message_();
uint8_t next_id_();
#ifdef DUCO_DEVICE_COUNT
StaticVector<DucoPendingRequest, DUCO_DEVICE_COUNT> pending_;
#endif
std::array<uint8_t, MAX_FRAME_SIZE> rx_buffer_{};
uint32_t last_byte_{0};
uint8_t rx_size_{0};
uint8_t last_id_{10};
bool escaped_{false}; // the last 0xAA was followed by a stuffing byte, so it starts no header
bool disable_crc_{false};
};
/// Every entity polls the box; deriving from PollingComponent shares its vtable.
class DucoDevice : public PollingComponent, public Parented<Duco> {
public:
virtual void receive_response(const DucoMessage &message) = 0;
};
/// Walks the node addresses once after boot; the nodes found are listed in the config dump.
class DucoDiscovery : public DucoDevice {
public:
void setup() override;
void dump_config() override;
void update() override; // one discovery step, run by the step interval
void receive_response(const DucoMessage &message) override;
protected:
static constexpr uint8_t NODE_COUNT = 0x45;
std::array<uint8_t, NODE_COUNT> node_types_{}; // 0 when no node answered at that address
uint8_t next_node_{0};
uint8_t attempts_{0};
};
#ifdef USE_TIME
class DucoTime : public PollingComponent, public Parented<Duco> {
public:
explicit DucoTime(time::RealTimeClock *clock) : clock_(clock) {}
void update() override;
protected:
time::RealTimeClock *clock_;
};
#endif
} // namespace esphome::duco
@@ -1,34 +0,0 @@
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS
from esphome.types import ConfigType
from .. import (
CONF_DUCO_ID,
DUCO_COMPONENT_SCHEMA,
DucoDevice,
duco_ns,
register_duco_device,
)
DEPENDENCIES = ["duco"]
CODEOWNERS = ["@kokx"]
DucoComfortTemperature = duco_ns.class_(
"DucoComfortTemperature", DucoDevice, number.Number
)
CONFIG_SCHEMA = (
number.number_schema(
DucoComfortTemperature,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
)
.extend(DUCO_COMPONENT_SCHEMA)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config: ConfigType) -> None:
var = await number.new_number(config, min_value=10.0, max_value=25.5, step=0.1)
await register_duco_device(var, config, config[CONF_DUCO_ID])
-26
View File
@@ -1,26 +0,0 @@
#include "number.h"
#include <cmath>
namespace esphome::duco {
static constexpr uint8_t PARAMETER_COMFORT_TEMPERATURE = 0x12;
static constexpr uint8_t GROUP_COMFORT_TEMPERATURE = 0x0a;
void DucoComfortTemperature::update() {
this->parent_->send(this, FUNCTION_PARAMETER, {0x00, PARAMETER_COMFORT_TEMPERATURE, GROUP_COMFORT_TEMPERATURE});
}
void DucoComfortTemperature::receive_response(const DucoMessage &message) {
// Reads and writes get the same answer: one byte in tenths of a degree, below 10.0 is invalid
if (message.size >= 4 && message.data[3] >= 100)
this->publish_state(message.data[3] / 10.0f);
}
void DucoComfortTemperature::control(float value) {
const auto tenths = static_cast<uint8_t>(lroundf(value * 10.0f));
this->parent_->send(this, FUNCTION_PARAMETER,
{0x01, PARAMETER_COMFORT_TEMPERATURE, GROUP_COMFORT_TEMPERATURE, tenths, 0x00, 0x00, 0x00});
}
} // namespace esphome::duco
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../duco.h"
namespace esphome::duco {
class DucoComfortTemperature : public DucoDevice, public number::Number {
public:
void update() override;
void receive_response(const DucoMessage &message) override;
protected:
void control(float value) override;
};
} // namespace esphome::duco
@@ -1,45 +0,0 @@
from esphome.components import select
import esphome.config_validation as cv
from esphome.types import ConfigType
from .. import (
CONF_DUCO_ID,
DUCO_COMPONENT_SCHEMA,
DucoDevice,
duco_ns,
register_duco_device,
)
DEPENDENCIES = ["duco"]
CODEOWNERS = ["@kokx"]
# Same order as MODE_CODES in select.cpp
DUCO_MODE_OPTIONS = [
"AUTO",
"MAN1",
"MAN2",
"MAN3",
"EMPT",
"CNT1",
"CNT2",
"CNT3",
"MAN1x2",
"MAN2x2",
"MAN3x2",
"MAN1x3",
"MAN2x3",
"MAN3x3",
]
DucoSelect = duco_ns.class_("DucoSelect", DucoDevice, select.Select)
CONFIG_SCHEMA = (
select.select_schema(DucoSelect)
.extend(cv.polling_component_schema("3s"))
.extend(DUCO_COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await select.new_select(config, options=DUCO_MODE_OPTIONS)
await register_duco_device(var, config, config[CONF_DUCO_ID])
-47
View File
@@ -1,47 +0,0 @@
#include "select.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::duco {
ESPHOME_LOG_TAG(TAG, "duco.select");
// Box code of each option, in the order of DUCO_MODE_OPTIONS in __init__.py
static constexpr uint8_t MODE_CODES[] PROGMEM = {
0x00, // AUTO
0x04, // MAN1
0x05, // MAN2
0x06, // MAN3
0x07, // EMPT
0x08, // CNT1
0x09, // CNT2
0x0a, // CNT3
0x84, // MAN1x2
0x85, // MAN2x2
0x86, // MAN3x2
0xc4, // MAN1x3
0xc5, // MAN2x3
0xc6, // MAN3x3
};
void DucoSelect::update() { this->parent_->send(this, FUNCTION_NODE, {0x02, 0x01}); }
void DucoSelect::receive_response(const DucoMessage &message) {
if (message.size < 1)
return;
for (size_t i = 0; i < sizeof(MODE_CODES); i++) {
if (progmem_read_byte(&MODE_CODES[i]) == message.data[0]) {
this->publish_state(i);
return;
}
}
ESP_LOGV(TAG, "Unknown mode %02X", message.data[0]);
}
void DucoSelect::control(size_t index) {
// The next update reads the new mode back
this->parent_->send(nullptr, FUNCTION_NODE, {0x04, 0x01, progmem_read_byte(&MODE_CODES[index])});
}
} // namespace esphome::duco
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../duco.h"
namespace esphome::duco {
class DucoSelect : public DucoDevice, public select::Select {
public:
void update() override;
void receive_response(const DucoMessage &message) override;
protected:
void control(size_t index) override;
};
} // namespace esphome::duco
-156
View File
@@ -1,156 +0,0 @@
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_CO2,
CONF_HUMIDITY,
CONF_TEMPERATURE,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PARTS_PER_MILLION,
UNIT_PERCENT,
UNIT_SECOND,
)
from esphome.types import ConfigType
from .. import (
CONF_DUCO_ID,
DUCO_COMPONENT_SCHEMA,
DucoDevice,
duco_ns,
register_duco_device,
)
DEPENDENCIES = ["duco"]
CODEOWNERS = ["@kokx"]
UNIT_DAYS = "d"
CONF_BYPASS = "bypass"
CONF_FILTER_REMAINING = "filter_remaining"
CONF_FLOW_LEVEL = "flow_level"
CONF_TEMPERATURE_EHA = "temperature_eha"
CONF_TEMPERATURE_ETA = "temperature_eta"
CONF_TEMPERATURE_ODA = "temperature_oda"
CONF_TEMPERATURE_SUP = "temperature_sup"
CONF_TIME_REMAINING = "time_remaining"
DucoSensor = duco_ns.class_("DucoSensor", DucoDevice, sensor.Sensor)
DucoReading = duco_ns.enum("DucoReading", is_class=True)
BOX_NODE = 1
def _schema(update_interval: str = "60s", **kwargs) -> cv.Schema:
return sensor.sensor_schema(
DucoSensor, state_class=STATE_CLASS_MEASUREMENT, **kwargs
).extend(cv.polling_component_schema(update_interval))
def _node_schema(**kwargs) -> cv.All:
"""A list of sensors, one per node address."""
return cv.ensure_list(
_schema(**kwargs).extend({cv.Required(CONF_ADDRESS): cv.int_range(0, 68)})
)
_TEMPERATURE = {
"unit_of_measurement": UNIT_CELSIUS,
"accuracy_decimals": 1,
"device_class": DEVICE_CLASS_TEMPERATURE,
}
# key: (what is read, request argument or None for the node address, schema)
SENSORS = {
CONF_CO2: (
DucoReading.DUCO_READING_CO2,
None,
_node_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
),
),
CONF_TEMPERATURE: (
DucoReading.DUCO_READING_NODE_TEMPERATURE,
None,
_node_schema(**_TEMPERATURE),
),
CONF_HUMIDITY: (
DucoReading.DUCO_READING_HUMIDITY,
None,
_node_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
),
),
CONF_FILTER_REMAINING: (
DucoReading.DUCO_READING_FILTER_REMAINING,
0x30,
_schema(
"1200s",
unit_of_measurement=UNIT_DAYS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
),
),
CONF_FLOW_LEVEL: (
DucoReading.DUCO_READING_FLOW_LEVEL,
BOX_NODE,
_schema(unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0),
),
CONF_TIME_REMAINING: (
DucoReading.DUCO_READING_TIME_REMAINING,
BOX_NODE,
_schema(
unit_of_measurement=UNIT_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
),
),
CONF_TEMPERATURE_ODA: (
DucoReading.DUCO_READING_BOX_TEMPERATURE,
0x00,
_schema(**_TEMPERATURE),
),
CONF_TEMPERATURE_SUP: (
DucoReading.DUCO_READING_BOX_TEMPERATURE,
0x01,
_schema(**_TEMPERATURE),
),
CONF_TEMPERATURE_ETA: (
DucoReading.DUCO_READING_BOX_TEMPERATURE,
0x02,
_schema(**_TEMPERATURE),
),
CONF_TEMPERATURE_EHA: (
DucoReading.DUCO_READING_BOX_TEMPERATURE,
0x03,
_schema(**_TEMPERATURE),
),
CONF_BYPASS: (
DucoReading.DUCO_READING_BYPASS,
0x10,
_schema(unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0),
),
}
CONFIG_SCHEMA = DUCO_COMPONENT_SCHEMA.extend(
{cv.Optional(key): schema for key, (_, _, schema) in SENSORS.items()}
)
async def to_code(config: ConfigType) -> None:
for key, (reading, argument, _) in SENSORS.items():
if (confs := config.get(key)) is None:
continue
for conf in confs if isinstance(confs, list) else [confs]:
var = await sensor.new_sensor(
conf, reading, conf.get(CONF_ADDRESS, argument)
)
await register_duco_device(var, conf, config[CONF_DUCO_ID])
-78
View File
@@ -1,78 +0,0 @@
#include "sensor.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::duco {
ESPHOME_LOG_TAG(TAG, "duco.sensor");
enum class DucoValueFormat : uint8_t {
DUCO_VALUE_FORMAT_UINT8,
DUCO_VALUE_FORMAT_UINT16,
DUCO_VALUE_FORMAT_INT16,
};
/// A little endian field at offset in the answer. Values outside [minimum, maximum] are dropped;
/// the box reports those for sensors that are missing or still starting.
struct ReadingLayout {
uint8_t function;
uint8_t offset;
DucoValueFormat format;
uint8_t divisor;
int16_t minimum;
uint16_t maximum;
};
// Indexed by DucoReading
static constexpr ReadingLayout READINGS[] PROGMEM = {
{FUNCTION_NODE_DATA, 4, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 1, 300, 10000}, // CO2
{FUNCTION_NODE_DATA, 2, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 10, 0, 1000}, // node temperature
{FUNCTION_NODE_DATA, 6, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 100, 0, 10000}, // humidity
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 255}, // filter remaining
{FUNCTION_NODE, 2, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 255}, // flow level
{FUNCTION_NODE, 12, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 1, 0, 65535}, // time remaining
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_INT16, 10, -999, 1000}, // box temperature
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 100}, // bypass
};
static ReadingLayout layout_of(DucoReading reading) {
ReadingLayout layout;
progmem_memcpy(&layout, &READINGS[static_cast<uint8_t>(reading)], sizeof(layout));
return layout;
}
void DucoSensor::update() {
switch (layout_of(this->reading_).function) {
case FUNCTION_NODE_DATA:
this->parent_->send(this, FUNCTION_NODE_DATA, {0x01, this->argument_, 0x00, 0x49, 0x04});
break;
case FUNCTION_PARAMETER:
this->parent_->send(this, FUNCTION_PARAMETER, {0x00, this->argument_, 0x09});
break;
case FUNCTION_NODE:
this->parent_->send(this, FUNCTION_NODE, {0x02, this->argument_});
break;
}
}
void DucoSensor::receive_response(const DucoMessage &message) {
const ReadingLayout layout = layout_of(this->reading_);
const bool wide = layout.format != DucoValueFormat::DUCO_VALUE_FORMAT_UINT8;
if (message.size < layout.offset + (wide ? 2 : 1)) {
ESP_LOGV(TAG, "Answer of %u bytes is too short", message.size);
return;
}
const uint8_t *field = message.data + layout.offset;
int32_t value = field[0];
if (layout.format == DucoValueFormat::DUCO_VALUE_FORMAT_UINT16) {
value = encode_value_le<uint16_t>(field);
} else if (layout.format == DucoValueFormat::DUCO_VALUE_FORMAT_INT16) {
value = encode_value_le<int16_t>(field);
}
if (value >= layout.minimum && value <= layout.maximum)
this->publish_state(static_cast<float>(value) / layout.divisor);
}
} // namespace esphome::duco
-34
View File
@@ -1,34 +0,0 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "../duco.h"
namespace esphome::duco {
/// What a sensor reads; the request and where the value is in the answer are in READINGS in sensor.cpp.
enum class DucoReading : uint8_t {
DUCO_READING_CO2,
DUCO_READING_NODE_TEMPERATURE,
DUCO_READING_HUMIDITY,
DUCO_READING_FILTER_REMAINING,
DUCO_READING_FLOW_LEVEL,
DUCO_READING_TIME_REMAINING,
DUCO_READING_BOX_TEMPERATURE,
DUCO_READING_BYPASS,
};
class DucoSensor : public DucoDevice, public sensor::Sensor {
public:
/// argument is the node address, or the parameter number for a box parameter.
DucoSensor(DucoReading reading, uint8_t argument) : reading_(reading), argument_(argument) {}
void update() override;
void receive_response(const DucoMessage &message) override;
protected:
DucoReading reading_;
uint8_t argument_;
};
} // namespace esphome::duco
@@ -1,27 +0,0 @@
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.types import ConfigType
from .. import (
CONF_DUCO_ID,
DUCO_COMPONENT_SCHEMA,
DucoDevice,
duco_ns,
register_duco_device,
)
DEPENDENCIES = ["duco"]
CODEOWNERS = ["@kokx"]
DucoSerial = duco_ns.class_("DucoSerial", DucoDevice, text_sensor.TextSensor)
CONFIG_SCHEMA = (
text_sensor.text_sensor_schema(DucoSerial)
.extend(cv.polling_component_schema("300s"))
.extend(DUCO_COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await text_sensor.new_text_sensor(config)
await register_duco_device(var, config, config[CONF_DUCO_ID])
@@ -1,17 +0,0 @@
#include "serial.h"
#include <cstring>
namespace esphome::duco {
void DucoSerial::update() { this->parent_->send(this, FUNCTION_NODE_DATA, {0x01, 0x01, 0x00, 0x1a, 0x10}); }
void DucoSerial::receive_response(const DucoMessage &message) {
if (message.size > 2) {
// The serial number starts at byte 2 and is padded with zeros
const char *serial = reinterpret_cast<const char *>(message.data + 2);
this->publish_state(serial, strnlen(serial, message.size - 2));
}
}
} // namespace esphome::duco
@@ -1,15 +0,0 @@
#pragma once
#include "esphome/components/text_sensor/text_sensor.h"
#include "../duco.h"
namespace esphome::duco {
class DucoSerial : public DucoDevice, public text_sensor::TextSensor {
public:
void update() override;
void receive_response(const DucoMessage &message) override;
};
} // namespace esphome::duco
@@ -1,5 +1,5 @@
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_FAN)
#ifdef USE_ESP32
#include "ecocomfort2_fan.h"
#include "esphome/core/log.h"
@@ -138,4 +138,4 @@ void Ecocomfort2Fan::on_status() {
} // namespace esphome::ecocomfort2
#endif // USE_ESP32 && USE_FAN
#endif // USE_ESP32
@@ -1,7 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_FAN)
#ifdef USE_ESP32
#include "esphome/core/component.h"
#include "esphome/components/fan/fan.h"
@@ -23,4 +23,4 @@ class Ecocomfort2Fan : public fan::Fan, public Ecocomfort2Client, public Compone
} // namespace esphome::ecocomfort2
#endif // USE_ESP32 && USE_FAN
#endif // USE_ESP32
+6 -5
View File
@@ -80,12 +80,13 @@ def final_validate(config: ConfigType) -> None:
full_config = fv.full_config.get()
# Count sensors registered to this hub (IDs are resolved at final_validate stage)
hub_id = config[CONF_ID]
_get_data().sensor_counts[str(hub_id)] = len(
fv.platform_entries(
domain=SENSOR_DOMAIN, platform=DOMAIN, match={CONF_EMONTX_ID: hub_id}
)
hub_id = str(config[CONF_ID])
sensor_count = sum(
1
for s in full_config.get(SENSOR_DOMAIN, [])
if s.get("platform") == DOMAIN and str(s.get(CONF_EMONTX_ID)) == hub_id
)
_get_data().sensor_counts[hub_id] = sensor_count
# Ensure UART RX buffer size is large enough to handle data bursts from firmware
for uart_conf in full_config[UART_DOMAIN]:
+8 -64
View File
@@ -1,5 +1,4 @@
#include "epaper_spi.h"
#include <algorithm>
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
@@ -38,42 +37,6 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
return true;
}
bool EPaperBase::init_sent_frame_(size_t length) {
if (!this->is_using_partial_update_())
return false;
if (this->sent_.init(length))
return true;
ESP_LOGW(TAG, "No memory for the previous frame; partial updates will send the whole drawn area");
return false;
}
uint32_t EPaperBase::buffer_hash_() const {
uint32_t hash = FNV1_OFFSET_BASIS;
size_t index = 0;
while (index != this->buffer_.size()) {
size_t length;
const uint8_t *data = this->buffer_.get_span(index, length);
for (size_t i = 0; i != length; i++)
hash = fnv1a_hash_extend(hash, data[i]);
index += length;
}
return hash;
}
void EPaperBase::set_full_bounds_() {
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void EPaperBase::reset_bounds_() {
this->x_low_ = this->width_;
this->x_high_ = 0;
this->y_low_ = this->height_;
this->y_high_ = 0;
}
void EPaperBase::setup_pins_() const {
for (auto *pin : this->enable_pins_) {
pin->setup();
@@ -270,28 +233,17 @@ void EPaperBase::process_state_() {
// Refresh the whole panel even if nothing was drawn
this->full_update_requested_ = false;
this->update_count_ = 0;
this->set_full_bounds_();
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) {
this->set_state_(EPaperState::IDLE);
return;
}
if (this->is_using_partial_update_()) {
const uint32_t frame_hash = this->buffer_hash_();
if (this->update_count_ != 0 && this->shown_hash_valid_ && frame_hash == this->shown_hash_) {
ESP_LOGD(TAG, "Frame unchanged, refresh skipped");
this->reset_bounds_();
this->set_state_(EPaperState::IDLE);
return;
}
this->shown_hash_ = frame_hash;
}
this->drawn_since_hash_ = false;
// Valid again only once this frame is on the panel, so an abandoned update leaves nothing to match
this->shown_hash_valid_ = false;
// A full update resets update_count_ to zero, so a non-zero count marks a partial one
this->partial_ = this->update_count_ != 0;
this->sent_count_ = 0;
this->set_state_(EPaperState::RESET);
break;
case EPaperState::INITIALISE:
@@ -304,15 +256,10 @@ void EPaperBase::process_state_() {
if (!this->transfer_data()) {
return; // Not done yet, come back next loop
}
if (this->sent_.is_valid() && this->sent_count_ == this->sent_.size())
this->sent_valid_ = true;
// A draw during the chunked push may not have reached the panel, so the next update sends the whole frame
this->sent_hash_intact_ = this->is_using_partial_update_() && !this->drawn_since_hash_;
if (this->drawn_since_hash_) {
this->set_full_bounds_();
} else {
this->reset_bounds_();
}
this->x_low_ = this->width_;
this->x_high_ = 0;
this->y_low_ = this->height_;
this->y_high_ = 0;
this->set_state_(EPaperState::POWER_ON);
break;
case EPaperState::POWER_ON:
@@ -333,7 +280,6 @@ void EPaperBase::process_state_() {
break;
case EPaperState::DEEP_SLEEP:
this->deep_sleep();
this->shown_hash_valid_ = this->sent_hash_intact_;
this->status_clear_warning();
this->set_state_(EPaperState::IDLE);
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
@@ -419,7 +365,6 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) {
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
this->y_low_ = clamp_at_most(this->y_low_, y);
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
this->drawn_since_hash_ = true;
return true;
}
@@ -541,7 +486,6 @@ void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_star
this->x_high_ = clamp_at_least(this->x_high_, x_high);
this->y_low_ = clamp_at_most(this->y_low_, y_low);
this->y_high_ = clamp_at_least(this->y_high_, y_high);
this->drawn_since_hash_ = true;
}
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
+4 -27
View File
@@ -99,7 +99,10 @@ class EPaperBase : public Display,
// We store 8 pixels per byte
this->buffer_.fill(pixel_color);
this->mark_all_drawn_();
this->x_high_ = this->width_;
this->y_high_ = this->height_;
this->x_low_ = 0;
this->y_low_ = 0;
}
void clear() override {
@@ -136,25 +139,6 @@ class EPaperBase : public Display,
void wait_for_idle_(bool should_wait);
void abandon_update_();
bool init_buffer_(size_t buffer_length);
// Allocates the copy of the previous frame; only drivers that diff against it (needs_previous_frame) call this
bool init_sent_frame_(size_t length);
void record_sent_(size_t index, const uint8_t *data, size_t length) {
if (this->sent_.is_valid()) {
this->sent_.write(index, data, length);
this->sent_count_ += length;
}
}
// Whether the driver reads the previous frame (sent_) for its differential refresh
virtual bool needs_previous_frame() const { return false; }
// FNV-1a hash of the current frame buffer, to detect an unchanged frame without a full copy
uint32_t buffer_hash_() const;
void reset_bounds_();
void set_full_bounds_();
// For a draw that writes the whole buffer without going through rotate_coordinates_()
void mark_all_drawn_() {
this->set_full_bounds_();
this->drawn_since_hash_ = true;
}
void update_effective_transform_();
bool rotate_coordinates_(int &x, int &y);
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, bool big_endian,
@@ -216,13 +200,6 @@ class EPaperBase : public Display,
size_t buffer_length_;
size_t current_data_index_{}; // used by data transfer to track progress
split_buffer::SplitBuffer buffer_{};
split_buffer::SplitBuffer sent_{}; // the previous frame, kept only by drivers that diff against it
bool sent_valid_{}; // sent_ holds a whole frame
size_t sent_count_{}; // bytes recorded into sent_ by the update in progress
uint32_t shown_hash_{}; // hash of the frame on the panel, to skip an unchanged update
bool shown_hash_valid_{}; // shown_hash_ holds the hash of a frame whose refresh completed
bool sent_hash_intact_{}; // the buffer was unchanged by the end of the push of shown_hash_
bool drawn_since_hash_{}; // something was drawn after shown_hash_ was taken
GPIOPin *dc_pin_{};
GPIOPin *busy_pin_{};
GPIOPin *reset_pin_{};
@@ -18,7 +18,11 @@ void EPaper4bpp::fill(Color color) {
// We store 2 pixels per byte
this->buffer_.fill(pixel_color + (pixel_color << 4));
this->mark_all_drawn_();
// Whole buffer just changed; mark the entire canvas dirty.
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void EPaper4bpp::clear() {
@@ -71,7 +71,10 @@ void EPaperInkplate2::fill(Color color) {
for (size_t i = half_buffer; i < this->buffer_length_; i++)
this->buffer_[i] = red_byte;
this->mark_all_drawn_();
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void EPaperInkplate2::clear() { this->fill(COLOR_ON); }
@@ -30,7 +30,6 @@ void EPaperJD79660::fill(Color color) {
// We store 4 pixels per byte
this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6));
this->drawn_since_hash_ = true;
}
void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) {
@@ -7,12 +7,6 @@
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi.mono");
void EPaperMono::setup() {
EPaperBase::setup();
if (!this->is_failed() && this->needs_previous_frame())
this->init_sent_frame_(this->sent_frame_length());
}
void EPaperMono::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->cmd_data(0x22, {partial ? (uint8_t) 0xFF : (uint8_t) 0xF7});
@@ -72,8 +66,7 @@ bool HOT EPaperMono::transfer_data() {
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
const size_t row_start = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
size_t data_idx = row_start;
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->send_red_ && !this->send_red_as_image_ ? 0 : this->buffer_[data_idx++];
}
@@ -12,8 +12,6 @@ class EPaperMono : public EPaperBase {
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
void setup() override;
protected:
void refresh_screen(bool partial) override;
void power_on() override {}
@@ -21,8 +19,6 @@ class EPaperMono : public EPaperBase {
void deep_sleep() override;
bool reset() override;
virtual void set_window();
// Bytes in the comparison frame; the buffer's size unless a subclass sends the frame in another layout
virtual size_t sent_frame_length() const { return this->buffer_length_; }
bool transfer_data() override;
bool send_red_{true};
bool software_reset_{true}; // send 0x12 after the hardware reset
@@ -6,6 +6,21 @@
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1677");
void EPaperSSD1677::setup() {
EPaperMono::setup();
if (!this->is_failed())
this->init_comparison_frame_();
}
void EPaperSSD1677::init_comparison_frame_() {
if (!this->is_using_partial_update_())
return;
if (!this->sent_.init(this->plane_row_length_() * this->height_)) {
ESP_LOGW(TAG, "No memory for the comparison frame; partial updates will degrade unchanged areas");
}
}
void EPaperSSD1677::plane_row(size_t y, uint8_t *out) {
const size_t row_length = this->plane_row_length_();
@@ -67,7 +82,7 @@ bool HOT EPaperSSD1677::transfer_data() {
}
this->write_array(data, length);
if (this->plane_ == 1)
this->record_sent_(this->current_data_index_, data, length);
this->sent_.write(this->current_data_index_, data, length);
this->current_data_index_ += length;
if (this->current_data_index_ != plane_length && millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
@@ -26,12 +26,12 @@ class EPaperSSD1677 : public EPaperMono {
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
: EPaperMono(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
void setup() override;
protected:
// Reads the previous frame to copy it into the other RAM bank on a full refresh
bool needs_previous_frame() const override { return true; }
// Allocates the comparison frame as setup() would, for tests that run without a bus.
void init_comparison_frame_() { this->init_sent_frame_(this->sent_frame_length()); }
size_t sent_frame_length() const override { return this->plane_row_length_() * this->height_; }
// Allocates the comparison frame when partial updates are enabled. Separate from setup() so it
// can run without a bus.
void init_comparison_frame_();
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
@@ -39,7 +39,8 @@ class EPaperSSD1677 : public EPaperMono {
bool reset() override;
bool transfer_data() override;
uint8_t plane_{0}; // 0 while sending 0x26, 1 while sending 0x24
split_buffer::SplitBuffer sent_{}; // the frame last sent to 0x24, i.e. what the panel shows
uint8_t plane_{0}; // 0 while sending 0x26, 1 while sending 0x24
};
} // namespace esphome::epaper_spi
@@ -29,7 +29,10 @@ void EPaperSSD1677Gray4::fill(Color color) {
}
const uint8_t level = color_to_gray4(color);
this->buffer_.fill((uint8_t) (level | (level << 2) | (level << 4) | (level << 6)));
this->mark_all_drawn_();
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void HOT EPaperSSD1677Gray4::draw_pixel_at(int x, int y, Color color) {
@@ -80,14 +83,14 @@ bool HOT EPaperSSD1677Gray4::transfer_data() {
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
const size_t src_row = this->current_data_index_ * this->row_width_ + this->x_low_ / 4;
for (size_t i = 0; i != plane_row_length; i++)
bytes_to_send[i] = plane_byte(this->buffer_[src_row + 2 * i], this->buffer_[src_row + 2 * i + 1], first_pass);
// What the next partial update compares against: the high bits, as a black-and-white frame.
if (first_pass)
this->record_sent_(this->current_data_index_ * plane_row_length, bytes_to_send, plane_row_length);
// The OTP grayscale waveform treats data as inverted relative to monochrome
for (size_t i = 0; i != plane_row_length; i++)
bytes_to_send[i] = (uint8_t) ~bytes_to_send[i];
for (size_t i = 0; i != plane_row_length; i++) {
const uint8_t plane = plane_byte(this->buffer_[src_row + 2 * i], this->buffer_[src_row + 2 * i + 1], first_pass);
// The OTP grayscale waveform treats data as inverted relative to monochrome
bytes_to_send[i] = (uint8_t) ~plane;
// What the next partial update compares against: the high bits, as a black-and-white frame.
if (first_pass && this->sent_.is_valid())
this->sent_[this->current_data_index_ * plane_row_length + i] = plane;
}
++this->current_data_index_;
this->write_array(bytes_to_send, plane_row_length);
if (millis() - start_time > MAX_TRANSFER_TIME) {
@@ -1,50 +1,10 @@
#include "epaper_spi_ssd1681.h"
#include <algorithm>
#include <cstring>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1681");
void EPaperSSD1681::bounds_from_changes_() {
if (!this->sent_valid_ || this->sent_.size() != this->buffer_.size())
return;
bool changed = false;
uint16_t row_low = this->height_, row_high = 0, col_low = this->row_width_, col_high = 0;
for (uint16_t row = 0; row != this->height_; row++) {
const size_t base = row * this->row_width_;
const size_t end = base + this->row_width_;
size_t index = base;
while (index != end) {
size_t new_length, old_length;
const uint8_t *new_bytes = this->buffer_.get_span(index, new_length);
const uint8_t *old_bytes = this->sent_.get_span(index, old_length);
const size_t length = std::min({new_length, old_length, end - index});
if (std::memcmp(new_bytes, old_bytes, length) != 0) {
for (size_t i = 0; i != length; i++) {
if (new_bytes[i] == old_bytes[i])
continue;
const auto col = static_cast<uint16_t>(index + i - base);
changed = true;
row_low = std::min(row_low, row);
row_high = std::max<uint16_t>(row_high, row + 1);
col_low = std::min(col_low, col);
col_high = std::max<uint16_t>(col_high, col + 1);
}
}
index += length;
}
}
if (!changed)
return;
this->x_low_ = col_low * 8;
this->x_high_ = std::min<uint16_t>(col_high * 8, this->width_);
this->y_low_ = row_low;
this->y_high_ = row_high;
}
bool HOT EPaperSSD1681::transfer_data() {
if (this->current_data_index_ == 0 && this->send_red_) {
if (!this->partial_) {
@@ -55,7 +15,6 @@ bool HOT EPaperSSD1681::transfer_data() {
this->y_high_ = this->height_;
} else {
// 0x26 already holds the image on the panel: the controller copies 0x24 into it after each refresh
this->bounds_from_changes_();
this->set_window();
this->send_red_ = false;
}
@@ -16,10 +16,6 @@ class EPaperSSD1681 : public EPaperSSD1683 {
}
protected:
// Reads the previous frame to compute the changed window for a partial refresh
bool needs_previous_frame() const override { return true; }
// Shrinks the update bounds to the bytes that differ from the frame last sent
void bounds_from_changes_();
bool transfer_data() override;
void refresh_screen(bool partial) override;
};
@@ -71,15 +71,12 @@ bool HOT EPaperSSD1683::transfer_data() {
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
const size_t row_start = this->current_data_index_ * this->row_width_ + this->x_low_;
size_t data_idx = row_start;
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->buffer_[data_idx++];
}
++this->current_data_index_;
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
if (!this->send_red_)
this->record_sent_(row_start, &bytes_to_send.front(), row_length);
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->disable();
@@ -232,7 +232,6 @@ void EPaperT133A01::fill(Color color) {
}
auto pixel_color = color_to_index(color);
this->buffer_.fill(pixel_color + (pixel_color << 4));
this->drawn_since_hash_ = true;
}
void EPaperT133A01::draw_pixel_at(int x, int y, Color color) {
@@ -1,5 +1,4 @@
#include "epaper_waveshare_bwr.h"
#include "esphome/core/helpers.h"
#include <algorithm>
@@ -37,9 +36,17 @@ void EPaperWaveshareBWR::draw_pixel_at(int x, int y, Color color) {
const auto bwr = color_to_bwr(color);
assign_bits(this->buffer_[pos], bit, bwr == BwrState::BWR_BLACK);
if (bwr == BwrState::BWR_BLACK) {
this->buffer_[pos] |= bit;
} else {
this->buffer_[pos] &= ~bit;
}
assign_bits(this->buffer_[red_offset + pos], bit, bwr == BwrState::BWR_RED);
if (bwr == BwrState::BWR_RED) {
this->buffer_[red_offset + pos] |= bit;
} else {
this->buffer_[red_offset + pos] &= ~bit;
}
}
void EPaperWaveshareBWR::fill(Color color) {
+5 -2
View File
@@ -1,7 +1,6 @@
#include "es8156.h"
#include "es8156_const.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -101,7 +100,11 @@ bool ES8156::set_mute_state_(bool mute_state) {
ESP_LOGV(TAG, "Read ES8156_REG13_DAC_MUTE: %u", reg13);
assign_bits(reg13, BIT(1) | BIT(2), mute_state);
if (mute_state) {
reg13 |= BIT(1) | BIT(2);
} else {
reg13 &= ~(BIT(1) | BIT(2));
}
ESP_LOGV(TAG, "Setting ES8156_REG13_DAC_MUTE to %u (muted: %s)", reg13, YESNO(mute_state));
return this->write_byte(ES8156_REG13_DAC_MUTE, reg13);
+10 -3
View File
@@ -1,7 +1,6 @@
#include "es8311.h"
#include "es8311_const.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -155,7 +154,11 @@ bool ES8311::configure_clock_() {
// Register 0x06
uint8_t reg06;
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, &reg06));
assign_bits(reg06, BIT(5), this->sclk_inverted_);
if (this->sclk_inverted_) {
reg06 |= BIT(5);
} else {
reg06 &= ~BIT(5);
}
reg06 &= 0xE0;
if (coefficient->bclk_div < 19) {
reg06 |= (coefficient->bclk_div - 1) << 0;
@@ -220,7 +223,11 @@ bool ES8311::set_mute_state_(bool mute_state) {
return false;
}
assign_bits(reg31, BIT(6) | BIT(5), mute_state);
if (mute_state) {
reg31 |= BIT(6) | BIT(5);
} else {
reg31 &= ~(BIT(6) | BIT(5));
}
return this->write_byte(ES8311_REG31_DAC, reg31);
}
+5 -2
View File
@@ -2,7 +2,6 @@
#include <cinttypes>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::es8388 {
@@ -185,7 +184,11 @@ bool ES8388::set_mute_state_(bool mute_state) {
// DAC settings that must be preserved. Previously muting overwrote the whole
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
// could not be unmuted again.
assign_bits(value, ES8388_DACCONTROL3_DAC_MUTE, mute_state);
if (mute_state) {
value |= ES8388_DACCONTROL3_DAC_MUTE;
} else {
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
}
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
return this->write_byte(ES8388_DACCONTROL3, value);
+1 -21
View File
@@ -56,7 +56,6 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError, HexInt
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.coroutine import CoroPriority, coroutine_with_priority
from esphome.espidf import sbom
from esphome.espidf.component import IDFComponent, generate_idf_components
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
@@ -3829,20 +3828,6 @@ def _write_sdkconfig():
clean_build(clear_pio_cache=False)
def _arduino_provides(name: str) -> bool:
"""Whether arduino-esp32 bundles the library a manifest depends on, so it is
never fetched by name; it still has to be enabled to build."""
if name not in ARDUINO_DISABLED_LIBRARIES:
return False
if name not in CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES]:
_LOGGER.warning(
"A library depends on the Arduino library %s, which is not enabled; "
"add it under esphome: libraries:",
name,
)
return True
def _write_idf_component_yml():
yml_path = CORE.relative_build_path("src/idf_component.yml")
dependencies: dict[str, dict] = {}
@@ -3864,11 +3849,7 @@ def _write_idf_component_yml():
# IDF sees the same requirement twice; converted components reach it through
# ${ESPHOME_PROJECT_MANAGED_COMPONENTS}.
managed = set(CORE.data[KEY_ESP32].get(KEY_COMPONENTS, {}))
converted = generate_idf_components(
libraries,
managed=managed,
provides=_arduino_provides if CORE.using_arduino else None,
)
converted = generate_idf_components(libraries, managed=managed)
# IDF names a component after its directory and a later registration of the
# same name replaces the earlier one, so a stub beside a converted library of
# the same name (espressif/libsodium vs esphome/libsodium) would win or lose
@@ -3994,7 +3975,6 @@ def copy_files():
CORE.relative_build_path("version.txt"),
__version__,
)
sbom.write_manifests()
# Remote extra build files are fetched into the shared download cache in
# one parallel batch (conditional requests skip unchanged files), then
-1
View File
@@ -667,7 +667,6 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
}
// Ignore these GAP events as they are not relevant for our use case
case ESP_GAP_BLE_KEY_EVT:
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
@@ -23,7 +23,7 @@ from esphome.components.openthread.const import CONF_POLL_PERIOD
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
from esphome.components.zigbee import DOMAIN as ZIGBEE_DOMAIN
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_OPENTHREAD
from esphome.const import CONF_ID, CONF_OPENTHREAD, CONF_PLATFORM
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -204,16 +204,11 @@ async def to_code(config: ConfigType) -> None:
add_idf_sdkconfig_option("CONFIG_IEEE802154_SLEEP_ENABLE", True)
_validate_single_instance = fv.unique_platform_key(
domain=power_management.DOMAIN,
platform=DOMAIN,
message=f"Only one {DOMAIN} instance is allowed",
)
def _final_validate(config: ConfigType) -> ConfigType:
_validate_single_instance(config)
full_config = fv.full_config.get()
entries = full_config.get(power_management.DOMAIN, [])
if sum(entry.get(CONF_PLATFORM) == DOMAIN for entry in entries) > 1:
raise cv.Invalid(f"Only one {DOMAIN} instance is allowed")
boot_freq = full_config[esp32.DOMAIN][esp32.CONF_CPU_FREQUENCY]
boot_mhz = int(boot_freq[:-3])
max_mhz = config.get(CONF_MAX_FREQUENCY)
@@ -157,9 +157,14 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi
}
void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
// protect from refreshing too often
uint32_t now = micros();
if (this->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
auto rate = this->max_refresh_rate_.value_or(0);
if (rate != 0 && (now - this->last_refresh_) < rate) {
// try again next loop iteration, so that this change won't get lost
this->schedule_show();
return;
}
this->last_refresh_ = now;
this->mark_shown_();
@@ -212,6 +217,17 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
}
}
light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const {
const light::ChannelColors &colors = this->channel_colors_;
uint8_t *led = this->buf_ + (index * colors.bytes_per_led());
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
&this->effect_data_[index],
&this->correction_};
}
void ESP32RMTLEDStripLightOutput::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 RMT LED Strip:\n"
@@ -223,7 +239,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() {
" Channel colors: %s\n"
" Max refresh rate: %" PRIu32 "\n"
" Number of LEDs: %u",
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_, this->num_leds_);
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_);
}
float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -29,7 +29,15 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
float get_setup_priority() const override;
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->channel_colors_.has_white()) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
}
return traits;
}
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->invert_out_ = inverted; }
@@ -47,16 +55,14 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; }
void clear_effect_data() override {
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->buf_, this->effect_data_, index, &this->correction_);
}
light::ESPColorView get_view_internal(int32_t index) const override;
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
@@ -80,7 +86,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
uint32_t max_refresh_rate_{0};
optional<uint32_t> max_refresh_rate_{};
};
} // namespace esphome::esp32_rmt_led_strip
@@ -30,13 +30,6 @@ from esphome.const import (
CONF_SAFE_MODE,
CONF_VERSION,
CONF_WEB_SERVER,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
PLATFORM_LN882X,
PLATFORM_RP2,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
@@ -283,31 +276,6 @@ def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType:
)
# Platforms with an OTA flash backend (ota_backend_factory.h)
_SUPPORTED_PLATFORMS = [
PLATFORM_ESP8266,
PLATFORM_ESP32,
PLATFORM_RP2,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
PLATFORM_HOST,
]
def _validate_platform(config: ConfigType) -> ConfigType:
if CORE.target_platform in _SUPPORTED_PLATFORMS:
return config
hint = (
f"; use '{CONF_PLATFORM}: zephyr_mcumgr' for OTA updates on nRF52"
if CORE.is_nrf52
else ""
)
raise cv.Invalid(
f"'{CONF_PLATFORM}: {DOMAIN}' is not supported on {CORE.target_platform}{hint}"
)
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for OTA component."""
from esphome.components import socket
@@ -319,7 +287,6 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType:
CONFIG_SCHEMA = cv.All(
_validate_platform,
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
@@ -1,4 +1,3 @@
from dataclasses import dataclass, field
import logging
from pathlib import Path
from typing import Any
@@ -25,33 +24,6 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "external_components"
@dataclass
class ExternalSource:
"""Origin of the components found in one source's components directory."""
components_dir: Path
url: str | None = None
ref: str | None = None
# Root of the git checkout holding components_dir
repo_dir: Path | None = None
@dataclass
class ExternalComponentsData:
sources: list[ExternalSource] = field(default_factory=list)
def _get_data() -> ExternalComponentsData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = ExternalComponentsData()
return CORE.data[DOMAIN]
def get_sources() -> list[ExternalSource]:
"""Return the sources processed for this configuration."""
return _get_data().sources
CONFIG_SCHEMA = cv.ensure_list(
{
cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA,
@@ -95,15 +67,6 @@ def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> P
"Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder"
)
get_sources().append(
ExternalSource(
components_dir=components_dir,
# Ends up in SBOMs that are shared outside
url=git.strip_url_credentials(config[CONF_URL]),
ref=config.get(CONF_REF),
repo_dir=repo_dir,
)
)
return components_dir
@@ -143,7 +106,6 @@ def _process_single_config(config: dict[str, Any]) -> None:
)
elif conf[CONF_TYPE] == TYPE_LOCAL:
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
get_sources().append(ExternalSource(components_dir=components_dir))
else:
raise NotImplementedError
-46
View File
@@ -48,16 +48,12 @@ void EzoPMP::update() {
}
if (this->is_first_read_) {
#ifdef USE_TEXT_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, (bool) this->calibration_status_);
#endif
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, (bool) this->max_flow_rate_);
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0,
(bool) this->absolute_total_volume_dosed_);
#endif
this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true);
this->is_first_read_ = false;
}
@@ -65,7 +61,6 @@ void EzoPMP::update() {
if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) {
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
if (this->is_dosing_flag_) {
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_);
@@ -74,7 +69,6 @@ void EzoPMP::update() {
}
this->queue_command_(EZO_PMP_COMMAND_READ_PUMP_VOLTAGE, 0, 0, (bool) this->pump_voltage_);
#endif
} else {
ESP_LOGV(TAG, "Not Scheduling new Command during update()");
}
@@ -198,68 +192,51 @@ void EzoPMP::read_command_result_() {
if (parsed_third_parameter.has_value())
this->is_dosing_flag_ = parsed_third_parameter.value_or(0) == 1;
#ifdef USE_BINARY_SENSOR
if (this->is_dosing_)
this->is_dosing_->publish_state(this->is_dosing_flag_);
#endif
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->last_volume_requested_) {
this->last_volume_requested_->publish_state(parsed_second_parameter.value_or(0));
}
#endif
#ifdef USE_TEXT_SENSOR
if (!this->is_dosing_flag_ && !this->is_paused_flag_) {
// If pump is not paused and not dispensing
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
}
#endif
break;
case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53)
#ifdef USE_SENSOR
if (parsed_first_parameter.has_value() && (bool) this->current_volume_dosed_) {
this->current_volume_dosed_->publish_state(parsed_first_parameter.value_or(0));
}
#endif
break;
case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: // Constant Flow Rate (page 57)
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->max_flow_rate_)
this->max_flow_rate_->publish_state(parsed_second_parameter.value_or(0));
#endif
break;
case EZO_PMP_COMMAND_READ_PAUSE_STATUS: // Pause (page 61)
if (parsed_second_parameter.has_value())
this->is_paused_flag_ = parsed_second_parameter.value_or(0) == 1;
#ifdef USE_BINARY_SENSOR
if (this->is_paused_)
this->is_paused_->publish_state(this->is_paused_flag_);
#endif
break;
case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64)
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->total_volume_dosed_)
this->total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0));
#endif
break;
case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64)
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->absolute_total_volume_dosed_)
this->absolute_total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0));
#endif
break;
case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: // Calibration (page 65)
#ifdef USE_TEXT_SENSOR
if (parsed_second_parameter.has_value() && this->calibration_status_) {
if (parsed_second_parameter.value_or(0) == 1) {
this->calibration_status_->publish_state("Fixed Volume");
@@ -271,56 +248,41 @@ void EzoPMP::read_command_result_() {
this->calibration_status_->publish_state("Uncalibrated");
}
}
#endif
break;
case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: // Pump Voltage (page 67)
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->pump_voltage_)
this->pump_voltage_->publish_state(parsed_second_parameter.value_or(0));
#endif
break;
// Non-Read Commands
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME);
#endif
break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME);
#endif
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE)
this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE);
#endif
break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS)
this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS);
#endif
break;
case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62)
this->is_paused_flag_ = false;
#ifdef USE_BINARY_SENSOR
if (this->is_paused_)
this->is_paused_->publish_state(this->is_paused_flag_);
#endif
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
#endif
break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS:
@@ -527,33 +489,25 @@ void EzoPMP::find() { this->queue_command_(EZO_PMP_COMMAND_FIND, 0, 0, true); }
void EzoPMP::dose_continuously() {
this->queue_command_(EZO_PMP_COMMAND_DOSE_CONTINUOUSLY, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
#endif
}
void EzoPMP::dose_volume(double volume) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME, volume, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
#endif
}
void EzoPMP::dose_volume_over_time(double volume, int duration) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME, volume, duration, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
#endif
}
void EzoPMP::dose_with_constant_flow_rate(double volume, int duration) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE, volume, duration, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
#endif
}
void EzoPMP::set_calibration_volume(double volume) {
@@ -21,11 +21,6 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
platform="gp8403", hub_key=CONF_GP8403_ID
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -22,9 +22,3 @@ def validate_interrupt_pin(value: ConfigType) -> ConfigType:
f"Remove the '{CONF_INTERRUPT_PIN}' to fall back to polling."
)
return value
# Schema of a CachedGpioExpanderComponent; to_code sets the pin with cg.add_pin().
INTERRUPT_EXPANDER_SCHEMA = cv.COMPONENT_SCHEMA.extend(
{cv.Optional(CONF_INTERRUPT_PIN): validate_interrupt_pin}
)
@@ -1,29 +0,0 @@
#include "cached_gpio_component.h"
namespace esphome::gpio_expander {
// Not a member of the template: GCC 10 (ESP8266) ignores section attributes on template instantiations.
static void IRAM_ATTR interrupt_pin_isr(Component *arg) { arg->enable_loop_soon_any_context(); }
bool InterruptExpanderComponent::attach_interrupt_pin_() {
// Polling mode enables the loop in pin_mode() for the first input; interrupt mode from the ISR.
this->disable_loop();
if (this->interrupt_pin_ == nullptr)
return false;
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&interrupt_pin_isr, static_cast<Component *>(this),
gpio::INTERRUPT_FALLING_EDGE);
// An INT line already low at attach gives no falling edge, so read the inputs now
if (!this->interrupt_pin_->digital_read())
this->enable_loop();
return true;
}
// Input changes that straddle the read leave INT asserted without a new falling edge, so keep looping
// until the line is released or the expander would keep stale state forever.
void InterruptExpanderComponent::disable_loop_when_released_() {
if (this->interrupt_pin_->digital_read())
this->disable_loop();
}
} // namespace esphome::gpio_expander
@@ -1,50 +0,0 @@
#pragma once
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome::gpio_expander {
/// Component side of a cached GPIO expander with an optional active-low INT line. Not a template, so the
/// ISR and the shared code are emitted once for every expander.
class InterruptExpanderComponent : public Component {
public:
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
float get_setup_priority() const override { return setup_priority::IO; }
protected:
/// Attach the falling-edge ISR when there is an interrupt pin, then disable the loop until an input
/// needs it. Returns whether the interrupt pin is in use.
bool attach_interrupt_pin_();
/// The loop runs again after the INT line falls; stop it once the line is released (HIGH).
/// Only called with an interrupt pin.
void disable_loop_when_released_();
/// Call from pin_mode() for an input pin: without an interrupt pin the loop polls instead.
void enable_polling_loop_() {
if (this->interrupt_pin_ == nullptr)
this->enable_loop();
}
InternalGPIOPin *interrupt_pin_{nullptr};
};
/// Base for expanders that cache reads per loop. With an interrupt pin the cache stays valid until INT fires.
template<typename T, uint16_t N>
class CachedGpioExpanderComponent : public InterruptExpanderComponent, public CachedGpioExpander<T, N> {
public:
void loop() override {
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr)
this->disable_loop_when_released_();
}
protected:
/// Call at the end of setup().
void setup_interrupt_pin_() {
if (this->attach_interrupt_pin_())
this->set_invalidate_on_read_(false);
}
};
} // namespace esphome::gpio_expander
+5 -2
View File
@@ -1,6 +1,5 @@
#include "gree.h"
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/helpers.h"
namespace esphome::gree {
@@ -25,7 +24,11 @@ void GreeClimate::set_model(Model model) {
}
void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
assign_bits(this->mode_bits_, bit_mask, enabled);
if (enabled) {
this->mode_bits_ |= bit_mask;
} else {
this->mode_bits_ &= ~bit_mask;
}
this->transmit_state();
}
Loaded 100 of 599 files, more files were not shown because too many files have changed in this diff. Show more