Compare commits

..
Author SHA1 Message Date
clydebarrow 968a010448 Merge branch 'pi07' into pi08 2026-10-25 18:06:19 +11:00
clydebarrow 4d299da205 Merge branch 'pi06' into pi07 2026-10-25 18:06:18 +11:00
clydebarrow c691a71cc5 Merge branch 'pi05' into pi06 2026-10-25 18:06:18 +11:00
clydebarrow 07164fc8b1 Merge remote-tracking branch 'upstream/dev' into pi05
# Conflicts:
#	esphome/components/packet_interface/__init__.py
#	esphome/core/defines.h
2026-10-25 18:06:16 +11:00
clydeandClaude Opus 5.5 fe19e264ab [entity_transport] Reject a timeout on a link that does the session's job
TCP provides the session itself, so its config has no session key and
the timeout check let it through. update_interval then defaults to
never, and unchanged states timed out on a working link. Record the
layers each link class provides and check those too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 9f57216b9d [sx126x][sx127x] Stop reading a radio that fails while sends are held back
Also drop a leftover preamble_detect key from a test case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 923893f6df [packet_interface] Name a peer in the error for a link used with peers
The link may have only one peer, so the error no longer says several.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydeandClaude Opus 5.5 a77653a0b3 [tcp] Apply clang-format to the packet interface test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 6d3b256191 [sx127x] Count only the sync word match as a packet arriving without LoRa
The preamble flag stays set when no sync word follows, so a packet from
another network held every send back until max_busy ran out. A link
without LoRa now needs a sync_value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 72f767cb4f [packet_interface] Log packets and fragments too short for their header
Every other drop in these layers is logged at verbose level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 861e0aa0da [packet_interface] Share the search for a layer that needs a session between both checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 f3e727dc48 [sx126x] Require a sync word for a link with FSK
Without one the radio cannot tell that a packet is arriving, so listen
before talk does nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 681d93ddba [tcp] Explain why the rolling code cannot be used on a TCP link
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 36799051e4 [sx126x][sx127x] Fixes from code review
- Without LoRa, require the SX127x radio to have a sync word or preamble
  detection, as listen before talk cannot see a packet arriving otherwise
- Warn once when a send finds the radio failed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 dd26efb0ca [tcp] Fixes from code review
- Publish the connected sensor as off at setup, so a link that never
  comes up does not show as unknown
- Add a timeout option, defaulting to three keep-alive intervals and
  longer than one, as the peer's keep_alive sets how often packets
  arrive; limit keep_alive to 1 h and timeout to 3 h
- Load binary_sensor for an empty config from tooling
- Name the layer in the error when a provided layer is configured

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 d4d91bc9d1 [sx126x][sx127x] Fixes from code review
- Warn when a send goes ahead over a busy period at its limit, as that
  means a stuck busy indication or a jammed channel
- Log an empty packet at verbose level, and say why it counts as sent
- Name the radio id if code generation cannot find its configuration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Code 8b5e3a1739 [tcp] Take the packet size from the receive buffer
The link takes the buffer as a std::span and derives max_packet_size
from its size, so a length beyond the buffer is refused and the
connection is closed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 ff6d33b39b [sx126x][sx127x] Say which callers send a refused packet again
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 df58794be0 [packet_interface] Keep App alive across tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 b0632892c5 [sx126x][sx127x] Require rx_start for layers that need replies
- Reject a radio with rx_start false when the link or a peer has a layer
  that needs packets back from the peer, as the radio sleeps after sending
- Log a send over a long busy period at debug level, so a lost reception
  can be traced

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 01e7827898 [packet_interface] Fix clang-tidy findings in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 dcc7c92a71 [sx126x][sx127x] Share the radio link code, and fixes from code review
- Make both links one RadioLink template in packet_interface, with shared
  code generation in radio_link.py, so a fix applies to both radios
- While sending is held back, watch the radio from loop() and end the
  busy period once it is free, so a later reception starts a new period
  instead of being sent over
- Reject an SX127x link without LoRa unless the radio is in packet mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 de5652f845 [tcp] Fixes from code review
- Reserve the link's sockets, so the socket pool is sized for them
- Test the server role: accepting a client, receiving from it, and
  accepting the next after it disconnects
- Test a frame assembled across several reads, after a keep-alive

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Code 4614562856 [sx126x][sx127x] Take the packet size at construction
The radio links have no buffer of their own, so code generation passes
the configured max_packet_size to the constructor.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 4a26a38cee [tcp] Keep the receive buffer in static memory
Code generation declares the TCP link's receive buffer as a static array
of max_packet_size bytes, so it is not allocated at run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 de6a9d5820 [sx126x][sx127x] Fixes from code review
- Hold a send back for at most the airtime of the largest packet, worked
  out from the radio settings and at least 2 s, instead of a fixed 2 s
  that a LoRa packet at a high spreading factor outlasts
- End a busy period when a send finds the radio free, so a busy flag that
  never clears is released however rarely sends are tried
- Detect an FSK reception on the SX127x from its preamble and sync word
  flags
- Test the radio links' final validation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 4e3f2b0e0b [packet_interface] Use a local to_vector() in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 cb28c2fc86 [sx126x][sx127x] Add packet interface links for SX126x and SX127x radios
Each packet is one radio packet, with the RSSI and SNR of received
packets in the metadata. A radio cannot hear while it sends, so a link
refuses to send while its radio is receiving a packet, for up to two
seconds, and the layers above send again shortly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 b04c8947e0 [tcp] Add TCP packet interface link
Packets go over one TCP connection, as a client that connects to a host
or a server that accepts one client at a time. TCP resends what is lost
and joins exactly two devices, so the link provides the reliability,
addressing and session layers itself, and those cannot be configured on
it. A link platform names such layers with the new provides argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:40 +11:00
clydebarrowandClaude Opus 5.5 b007bbb01c [packet_interface] Check at validation that fragments fit the reliability queue
- Work out each layer's packet size during validation, as the C++ does,
  and reject a fragmentation max_packet_size whose fragments do not all
  fit in the reliability queue at once, or need more than 128 fragments
- Fit the test peer's largest packet in its queue
- Reuse interface_layers() in final_validate_interface()
- Keep the earlier tests in place in test_packet_interface.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:39 +11:00
clydebarrowandClaude Opus 5.5 7ac9ba6daf [packet_interface] Use the shared max_packet_size constant in the fragmentation layer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:39 +11:00
clydebarrowandClaude Opus 5.5 8b752525f5 [entity_transport] Find an addressing peer's layers for the timeout check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:39 +11:00
clydebarrowandClaude Opus 5.5 07e1195ffa [packet_interface] Route broadcasts to the broadcast port, and keep a join
A broadcast from a device with its own port went to that port, whose
layers drop it, so a broadcast port could not reach reliable peers. It now
goes to the broadcast port when there is one.

Fragmentation allocates its buffers with RAMAllocator and fails setup when
they cannot be allocated. A fragment from another source no longer drops
the packet being joined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:39 +11:00
clydebarrowandClaude Code 5ef072d03d [packet_interface] Take the addressing and fragmentation sizes at construction
AddressingPort derives max_packet_size from the link in its constructor,
and the fragmentation layer passes its configured size to the base
constructor.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 13:39:39 +11:00
clydebarrowandClaude Opus 5.5 4d61784ba6 [packet_interface] Keep App alive across tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:39 +11:00
clydebarrowandClaude Opus 5.5 be4ea37f21 [packet_interface] Fix clang-tidy findings in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:39 +11:00
clydebarrowandClaude Opus 5.5 9915cf6827 [packet_interface] Fixes from code review
- Reject an addressing destination or peer that is this device's own
  address, whose packets would be dropped as echoes
- Show 0 fragments, not SIZE_MAX, for a link with no room
- Test the check that applications use a peer's id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:39 +11:00
clydebarrowandClaude Opus 5.5 beeeee07e8 [packet_interface] Use a local to_vector() in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:21 +11:00
clydebarrowandClaude Opus 5.5 e9c386f57e [packet_interface] Leave no room, instead of underflowing, on a small link
As in the other layers, addressing ports and fragmentation get no room
when max_packet_size is smaller than their header, and fragmentation
fails its setup instead of dividing by zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:21 +11:00
clydebarrowandClaude Opus 5.5 01fbfeb16a [packet_interface] Add addressing and fragmentation layers
The addressing layer lets several devices share a channel such as an
RS485 bus or a radio, with 16-bit addresses. A link can carry several
peers, each with its own stack of layers and its own id. Layers that work
between two devices only are rejected with broadcast addressing. The
fragmentation layer splits packets larger than the layers below allow,
and joins them again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:21 +11:00
pre-commit-ci-lite[bot] 369354a116 apply automatic formatting fixes 2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 105d1bb088 [entity_transport] Warn when packets are refused for long, and check every complete dictionary
- Set a warning once the interface has accepted no packets for a minute,
  and log refused dictionary packets as data packets are
- Clear remote entities that a provider's dictionary does not name also
  when the provider goes back to a dictionary already seen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 1488af2b32 [entity_transport] Fixes from code review
- Check a data packet before asking for its dictionary, so a damaged
  packet does not make the provider resend everything
- When a provider's complete dictionary does not name a remote entity,
  warn and clear that entity's state, as a typo in provider or remote_id,
  or an entity the provider no longer sends, was otherwise silent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 74b8de8064 [entity_transport] Ask again for a dictionary with a lost part, and warn on dropped packets
A dictionary packet now carries its part index and a last-part flag. A dictionary counts as received, and is not
asked for again, only once its last part follows every other part in order. Until then, data under it sends a
request even when some remote entities are already known.

A refused request no longer starts the request interval. A short or malformed packet, or one with another format
version, is logged as a warning at most once a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 5393d192b4 [entity_transport] Reject a timeout on a packet interface with a session layer
With a session and a reliability layer, the update interval defaults to
never, and unchanged states are not sent again, so a receiver's timeout
cleared them on a working link. The session layer already clears the
states when the other device cannot be reached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 670934b3d8 [entity_transport] Tag each data record, and check whole packets before use
Each data record now starts with a 16-bit tag holding the entity's number
and type, and the form of its payload, so that more entity types can be
added and a receiver can skip records of types it does not know. A binary
sensor's state is in the tag; a sensor has a 4 byte state, or none.

Data and dictionary packets are checked in full before anything in them
is used, so a damaged or forged packet changes no state and teaches nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 f76dce46aa [entity_transport] Use the shared provider and remote id constants
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 d44bccba3f [entity_transport] Ask only for dictionaries not yet received
A dictionary that names none of the remote entities was asked for again
every 5 seconds while any remote entity was unresolved, and each request
made its provider resend everything. A provider's new dictionary was not
asked for once every remote entity was resolved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 5b35282546 [entity_transport] Fail when the packet buffer cannot be allocated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Code aa1d7fa26b [entity_transport] Set the test link's size at construction
The mock link takes max_packet_size in its constructor, so a test that
needs a different size builds a local link.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 3862504cdd [packet_interface] Keep App alive across tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 ee3efdf15e [entity_transport] Brace multi-line if bodies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 db46ed33a2 [packet_interface] Fix clang-tidy findings in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 e09257a992 [packet_interface] Use a local to_vector() in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 1fa3dd37fe [entity_transport] Fixes from code review
- Compare received ids by length and bytes, so an id with an embedded NUL
  cannot read past the configured string
- Fail setup when max_packet_size cannot hold the device name and the
  longest entity id, instead of skipping the id at every resend
- Reject two entities of one type sent with the same id
- Avoid an unused variable without remote entities
- Use matching ids in the test configuration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydebarrowandClaude Opus 5.5 a7ca6a9608 [entity_transport] Add entity transport component
Sends sensor and binary sensor states to other devices over a packet
interface. A dictionary sent on connect, on request and at an interval
names the entities, so data packets refer to them by number. The update
interval and the handling of a lost peer follow the layers the interface
has, and remote states can time out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:03 +11:00
clydeandClaude Opus 5.5 e046551825 [packet_interface] Stretch the XXTEA key text with PBKDF2
A single SHA-256 of the key text let a captured packet check about 2^30
guesses for every one that passes, so a weak key text fell quickly to
an offline search. Derive the key with PBKDF2-HMAC-SHA256 at 600,000
iterations instead; this runs at build time, so the device pays nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:39:00 +11:00
pre-commit-ci-lite[bot] 18b5b7a24a apply automatic formatting fixes 2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 179d20ba77 [packet_interface] Limit the session keep_alive to 1 h and timeout to 3 h
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 1bc4779d96 [packet_interface] Fail the XXTEA layer when its buffers cannot be allocated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 366713ba05 [packet_interface] Check the layers in the link schema
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 47b8aae1b5 [packet_interface] Require a session layer with the rolling code
The rolling code accepts a packet only once the peer has sent back this
device's boot number, so a device that only receives never gets a packet.
The session's keep-alives make sure packets go both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
pre-commit-ci-lite[bot] a9c155e4b4 apply automatic formatting fixes 2026-10-23 12:28:10 +10:00
clydebarrowandClaude Code 708e117a20 [packet_interface] Take the session and encryption sizes at construction
Both layers derive max_packet_size from the layer below in their
constructors instead of assigning it in the body.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 c6d1c63884 [packet_interface] Keep App alive across tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 faef5a98bf [packet_interface] Fix clang-tidy findings in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 7c86318c1a [packet_interface] Use a local to_vector() in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 58ef6ef3d9 [packet_interface] Fixes from code review
- Fail setup, instead of underflowing, when max_packet_size leaves the
  encryption or session layer no room
- Warn when the boot number cannot be saved
- Warn once when the peer's packets carry an older boot number, which
  happens when its flash is erased
- Test the counter wrapping to a new boot number

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 a6d2ae15a1 [packet_interface] Add session and XXTEA encryption layers
The session layer sends keep-alives and reports whether the peer can be
reached, including after it restarts or a reliability layer below loses
data. The encryption layer encrypts and checks each packet with XXTEA,
with an optional rolling code against replays. Components that a layer
needs, such as binary_sensor, are loaded only when it is configured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 63cfdf2b4d [packet_interface] Reject a layer type registered after the schemas are built
Such a type would be missing from the link schema, and selecting it would
fail with an unrelated error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 6411a8062f [packet_interface] Log a full queue during a link reset only once
While the peer is away the layers above keep sending, so the message
repeated about once a second until the link came up again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 c18ac5bac9 [packet_interface] Count the link reset tries outside the condition
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 78396dd142 [packet_interface] Warn when the HDLC link cannot come up
A frame larger than max_packet_size, usually from a peer with a larger
setting, is warned about once. A link reset that the peer does not answer
is warned about after max_retries tries, and a full queue is logged at
debug level while the link is down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Code 557e3c3e9e [packet_interface] Reject a duplicate layer type name
Every module in layers/ is imported automatically, so two modules
registering the same name silently replaced one with the other, in an
order decided by pkgutil. Registration now raises, so the mistake is
found when the configuration is loaded.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Code 8fd05e93b4 [packet_interface] Answer a repeated SABM without resetting the link
A peer that has not heard the UA repeats its SABM, and cannot have sent a
numbered frame meanwhile. Treating that as a restart threw away queued
frames and reported the session lost for a single dropped UA. A SABM is
now a restart only if the peer has sent a numbered frame since the link
came up.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Code f81c6847f3 [packet_interface] Take the reliability layer's size at construction
max_packet_size is derived from the layer below in the constructor
instead of being assigned in the body.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 d555d64300 [packet_interface] Fixes from code review
- Keep App alive across the HDLC tests, as the test main requires
- Run a layer's loop in the tests only while it has asked for it, so
  the loop's disable and request bookkeeping is tested, and wait for the
  retry timer in the tests of refused sends
- Report no send capacity while the layer has no send queue

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 267e3d5d70 [packet_interface] Fix clang-tidy findings in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 2da760ea4f [packet_interface] Use a local to_vector() in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 0b34f811a4 [packet_interface] Fixes from code review
- Reset the link after max_retries even when it is 255, where the retry
  counter used to wrap
- Require a reliability timeout of at least 50 ms
- Drop received frames larger than the layer's max_packet_size
- Fail setup, instead of aborting, when the send queue cannot be
  allocated or max_packet_size leaves no room
- Test the ack delay timer and a refused REJ

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Opus 5.5 28833fe0f3 [packet_interface] Add layer registry and HDLC reliability layer
Layers stack on a link and are configured on its packet_interface entry.
Each layer type registers itself with its kind, so a new type needs only
a new module. The first is a reliability layer in the style of HDLC
asynchronous balanced mode, with go-back-N retransmission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:28:10 +10:00
clydebarrowandClaude Code 929aac8fac [udp] Take the packet size from the copy buffer
The link derives max_packet_size from the span it is given, so a packet
larger than the copy buffer cannot be copied into it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 13:28:08 +11:00
clydebarrowandClaude Opus 5.5 11cc5bb008 [udp] Limit a split packet to the copy buffer
The copy buffer is sized by code generation to max_packet_size, but
set_max_packet_size() can raise the limit afterwards, and copying a split
packet would then overflow it. The link now takes the buffer as a span
and refuses a split packet larger than it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:28:08 +11:00
clydebarrowandClaude Opus 5.5 2d6be6c27a [udp] Drop datagrams longer than the receive buffer
A read cuts a datagram longer than the buffer to fit, so a 600 byte
datagram arrived as a valid looking 508 byte packet. The buffer is now one
byte longer, and a read that fills it is dropped. The split packet test
also checks the bytes joined in the copy buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:28:08 +11:00
clydebarrowandClaude Opus 5.5 bfa6e68039 [packet_interface] Fix clang-tidy findings in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:28:08 +11:00
clydebarrowandClaude Opus 5.5 c39566f312 [udp] Keep the copy buffer in static memory
Code generation declares the UDP link's buffer for packets split across
buffers as a static array of max_packet_size bytes, so it is not
allocated at run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:28:08 +11:00
clydebarrowandClaude Opus 5.5 5197140f2c [packet_interface] Use a local to_vector() in tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:28:08 +11:00
clydebarrowandClaude Opus 5.5 ee0c6a412e [udp] Fixes from code review
- Enforce max_packet_size on send and receive
- Copy split packets into a buffer allocated in setup, not on the stack
- Add unit tests for the link's send and receive paths
- Explain why the broadcast socket is always enabled

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:28:08 +11:00
clydebarrowandClaude Opus 5.5 373c6398b2 [udp] Add UDP packet interface link
Each packet is one UDP datagram, using the settings of the udp component.
The udp component now reports whether a packet was sent, and an ENOMEM
refusal from lwIP is logged at verbose level as the packet can be sent
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:28:08 +11:00
1032 changed files with 17047 additions and 18940 deletions

No files matched your search

+3 -8
View File
@@ -217,10 +217,9 @@ file does, and it is the authority when they disagree. The most useful starting
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>`, 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)`.
`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)`.
* **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.
@@ -444,16 +443,12 @@ file does, and it is the authority when they disagree. The most useful starting
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state.
`publish="momentary"` is only for legacy action switches (restart, factory reset): it publishes off and runs
the body only when turned on. Never use it for a new entity; an action is a button.
```python
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
```
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
to still drive a state when the restore mode is disabled.
A hub that applies its switches' restored states itself reads `sw->get_initial_write_state()`, which
already applies `inverted`.
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
+5
View File
@@ -196,6 +196,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
esphome/components/ens160_i2c/* @latonita
esphome/components/ens160_spi/* @latonita
esphome/components/ens210/* @itn3rd77
esphome/components/entity_transport/* @clydebarrow
esphome/components/epaper_spi/* @esphome/core
esphome/components/es7210/* @kahrendt
esphome/components/es7243/* @Chorty
@@ -641,7 +642,9 @@ esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx126x/packet_interface/* @clydebarrow
esphome/components/sx127x/* @swoboda1337
esphome/components/sx127x/packet_interface/* @clydebarrow
esphome/components/sy6970/* @linkedupbits
esphome/components/syslog/* @clydebarrow
esphome/components/systa_bus/* @Mat931
@@ -653,6 +656,8 @@ esphome/components/tca8418/* @zebble
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tcp/* @clydebarrow
esphome/components/tcp/packet_interface/* @clydebarrow
esphome/components/tcp_uart/* @Bascht74
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax @FredM67
+10 -17
View File
@@ -1305,7 +1305,10 @@ def upload_program(
f"{option_string} is only supported with the esphome OTA platform; "
"the web_server OTA path can only update the firmware image."
)
return _upload_via_web_server(config, network_devices, _app_ota_image(args))
binary = CORE.firmware_bin
if getattr(args, "file", None) is not None:
binary = Path(args.file)
return _upload_via_web_server(config, network_devices, binary)
return _upload_via_native_api(config, network_devices, args)
@@ -1361,16 +1364,6 @@ def _esphome_ota_conf(config: ConfigType) -> ConfigType | None:
return next(iter(esphome_ota_items(config)), None)
def _app_ota_image(args: ArgsProtocol) -> Path:
"""The file an app update sends: ``--file`` or the build output, as the
target platform's ota_image hook picks it."""
binary = CORE.firmware_bin
if (file := getattr(args, "file", None)) is not None:
binary = Path(file)
ota_image = platform_hooks.get_platform_hook(CORE.target_platform, "ota_image")
return ota_image(binary) if ota_image is not None else binary
def _upload_via_native_api(
config: ConfigType, network_devices: list[str], args: ArgsProtocol
) -> tuple[int, str | None]:
@@ -1420,27 +1413,27 @@ def _upload_via_native_api(
f"flash a build with the option enabled, and then retry {option_string}."
)
binary = CORE.firmware_bin
ota_type = espota2.OTA_TYPE_UPDATE_APP
file = getattr(args, "file", None)
if getattr(args, "partition_table", False):
# Fail fast if the resolved ESPHome OTA config does not enable allow_partition_access.
# The device-side handshake also rejects this with "Device only supports app updates",
# but checking here surfaces the misconfiguration before opening a network connection.
check_partition_access("--partition-table")
binary = Path(file) if file is not None else CORE.partition_table_bin
binary = CORE.partition_table_bin
ota_type = espota2.OTA_TYPE_UPDATE_PARTITION_TABLE
elif getattr(args, "bootloader", False):
check_partition_access("--bootloader")
if (
file is None
getattr(args, "file", None) is None
and (native := native_backend())
and (hint := getattr(native, "missing_image_hint", lambda: None)())
):
raise EsphomeError(hint)
binary = Path(file) if file is not None else CORE.bootloader_bin
binary = CORE.bootloader_bin
ota_type = espota2.OTA_TYPE_UPDATE_BOOTLOADER
else:
binary = _app_ota_image(args)
if getattr(args, "file", None) is not None:
binary = Path(args.file)
if ota_type == espota2.OTA_TYPE_UPDATE_PARTITION_TABLE:
_validate_partition_table_binary(binary)
+1 -30
View File
@@ -1,18 +1,15 @@
from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass, field
import logging
import math
import string
from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ABOVE,
CONF_ALL,
CONF_ANY,
CONF_AUTOMATION_ID,
CONF_BELOW,
CONF_CONDITION,
CONF_COUNT,
CONF_ELSE,
@@ -366,25 +363,6 @@ class ApplyField:
return ApplyCall(target, ((self.conf_key, self.type_, self.const_fn),))
def _in_range_low(config: ConfigType, value: float) -> str:
return "-INFINITY" if math.isnan(value) else str(cg.safe_exp(value))
def _in_range_high(config: ConfigType, value: float) -> str:
# With no lower bound, an unset upper bound stays NaN so the check is false, as before.
if math.isnan(value):
return "NAN" if math.isnan(config[CONF_ABOVE]) else "INFINITY"
return str(cg.safe_exp(value))
# ``above <= state <= below`` on the parent's float ``state``; register with ``IN_RANGE_DEFAULTS``.
IN_RANGE_CHECK = ApplyCall(
"state >= {} && {parent}->state <= {}",
((CONF_ABOVE, cg.float_, _in_range_low), (CONF_BELOW, cg.float_, _in_range_high)),
)
IN_RANGE_DEFAULTS = {CONF_ABOVE: math.nan, CONF_BELOW: math.nan}
@dataclass(frozen=True)
class ApplyBytes:
"""A byte payload key forwarded as ``target``, whose ``{}`` stands for ``data, len``.
@@ -713,11 +691,7 @@ async def _render_check(
def register_apply_condition(
name: str,
schema: cv.Schema,
check: str | ApplyCall,
id_key: str = CONF_ID,
defaults: ConfigType | None = None,
name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID
) -> None:
"""Register a condition that is one expression on its parent, with no C++ class.
@@ -726,7 +700,6 @@ def register_apply_condition(
against config values, all of which must be present. Write ``== false`` to negate.
A ``cg.std_string`` constant stays in flash on ESP8266 and supports only ``==`` or ``!=``
against a ``std::string`` or ``StringRef`` member; other string types are plain literals.
``defaults`` fills absent keys at code generation only, so the validated config is unchanged.
Generates one static predicate and an ``ApplyCondition`` templated on it.
"""
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
@@ -741,8 +714,6 @@ def register_apply_condition(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
if defaults:
config = {**defaults, **config}
parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args)
values = _apply_values(config, members)
+13 -30
View File
@@ -7,7 +7,6 @@ import re
import textwrap
from esphome.build_helpers import pch
from esphome.build_helpers.ccache import CCACHE_PCH_MIN_VERSION
from esphome.components.esp32 import (
KEY_SRC_CMAKE,
get_esp32_variant,
@@ -516,7 +515,7 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
# Where CMake puts the .gch of the src component; ccache reads the checksum
# next to it in place of the .gch
_PCH_SUM_PATH = "build/esp-idf/src/CMakeFiles/__idf_src.dir/cmake_pch.hxx.gch.sum"
# Where the CMake gate records its choice
# Where the Windows gate records its choice
_PCH_CHOICE_VAR = "ESPHOME_PCH"
@@ -532,32 +531,16 @@ def _pch_cmake_block() -> str:
block = f"""target_precompile_headers(${{COMPONENT_LIB}} PRIVATE
{headers}
)"""
if pch.pch_forced():
if not pch.pch_needs_gcc_check():
return f"\n# ESPHome precompiled header\n{block}\n"
# Only CMake knows the compiler version before the first configure, and
# the ccache IDF found is the one the build runs
gcc_gate = ""
if pch.pch_needs_gcc_check():
gcc_gate = f"""if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
message(STATUS "ESPHome: GCC ${{CMAKE_CXX_COMPILER_VERSION}} cannot load a precompiled header on Windows; compiling without it")
set(_esphome_pch OFF)
endif()
"""
min_ccache = ".".join(map(str, CCACHE_PCH_MIN_VERSION))
# Before the first configure only CMake knows the compiler version
return f"""
# ESPHome precompiled header, unless a check below rules it out
set(_esphome_pch ON)
{gcc_gate}if(CCACHE_ENABLE AND CCACHE_FOUND)
execute_process(COMMAND "${{CCACHE_FOUND}}" --version OUTPUT_VARIABLE _esphome_ccache ERROR_QUIET)
if(_esphome_ccache MATCHES "ccache version ([0-9.]+)")
if(CMAKE_MATCH_1 VERSION_LESS {min_ccache})
message(STATUS "ESPHome: ccache ${{CMAKE_MATCH_1}} can reuse stale objects with a precompiled header; compiling without it (upgrade ccache to {min_ccache} or newer, or set ESPHOME_PCH_ENABLE=1 to force)")
set(_esphome_pch OFF)
endif()
endif()
endif()
set({_PCH_CHOICE_VAR} ${{_esphome_pch}} CACHE BOOL "ESPHome precompiled header in use" FORCE)
if(_esphome_pch)
# ESPHome precompiled header, unless GCC bug 14940 keeps it from loading
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
message(STATUS "ESPHome: GCC ${{CMAKE_CXX_COMPILER_VERSION}} cannot load a precompiled header on Windows; compiling without it")
set({_PCH_CHOICE_VAR} OFF CACHE BOOL "ESPHome precompiled header in use" FORCE)
else()
set({_PCH_CHOICE_VAR} ON CACHE BOOL "ESPHome precompiled header in use" FORCE)
{textwrap.indent(block, " ")}
endif()
"""
@@ -574,10 +557,10 @@ def write_pch_checksum() -> None:
if not pch.pch_enabled():
return
# Written whatever the gate chose last time: a build tree without that
# choice, or a reconfigure that turns the pch on, must not see a stale sum
if pch.pch_forced() or get_cmake_cache_value(_PCH_CHOICE_VAR) == "ON":
pch.log_pch_in_use()
# The gate's choice, cached by configure
if pch.pch_needs_gcc_check() and get_cmake_cache_value(_PCH_CHOICE_VAR) != "ON":
return
pch.log_pch_in_use()
checksum = pch.pch_checksum(
CORE.relative_src_path(),
pch.PCH_DEFAULT_HEADERS,
+1 -48
View File
@@ -3,68 +3,21 @@ resolution, and default ``CCACHE_*`` values."""
from __future__ import annotations
import functools
import logging
import os
from pathlib import Path
import re
from esphome.framework_helpers import (
strip_win_long_path_prefix,
tool_version_probe,
tool_version_runs,
)
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
from esphome.helpers import FALSY_ENV_STRINGS, TRUTHY_ENV_STRINGS
_LOGGER = logging.getLogger(__name__)
# Before 4.7 ccache stores a pch consumer whose header is newer than one
# second without that header, so a later build reuses it after the header
# changed (#21621)
CCACHE_PCH_MIN_VERSION = (4, 7)
_VERSION_RE = re.compile(r"ccache version (\d+(?:\.\d+)*)")
@functools.cache
def _ccache_probe(ccache: str) -> str | Exception:
"""``ccache --version``, run once per binary per process."""
return tool_version_probe(ccache)
def _ccache_runs(ccache: str) -> bool:
"""Return True when the ``ccache`` found on PATH actually runs."""
return tool_version_runs(
ccache,
"Ignoring ccache at %s because it failed to run; compiling without ccache",
probe=_ccache_probe,
)
@functools.cache
def _ccache_too_old_for_pch(ccache: str) -> bool:
output = _ccache_probe(ccache)
if not isinstance(output, str) or (found := _VERSION_RE.search(output)) is None:
return False
if tuple(map(int, found[1].split("."))) >= CCACHE_PCH_MIN_VERSION:
return False
_LOGGER.info(
"ccache %s at %s can reuse stale objects with a precompiled header; "
"compiling without it (upgrade ccache to %s or newer, or set "
"ESPHOME_PCH_ENABLE=1 to force)",
found[1],
ccache,
".".join(map(str, CCACHE_PCH_MIN_VERSION)),
)
return True
def ccache_breaks_pch() -> bool:
"""Whether the ccache this build uses is too old for a precompiled header.
ESP-IDF builds check the ccache IDF finds in CMake instead (build_gen.espidf)."""
return (ccache := resolve_ccache_path()) is not None and _ccache_too_old_for_pch(
ccache
)
+4 -19
View File
@@ -12,11 +12,7 @@ import re
import subprocess
import sys
from esphome.build_helpers.ccache import (
ccache_breaks_pch,
effective_ccache_basedir,
parse_enable_env,
)
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
from esphome.const import PLATFORM_NRF52
from esphome.helpers import write_file_if_changed
@@ -121,16 +117,9 @@ def pch_needs_gcc_check() -> bool:
return sys.platform == "win32" and parse_enable_env("ESPHOME_PCH_ENABLE") is None
def _ccache_rules_out_pch() -> bool:
"""An old ccache, unless ``ESPHOME_PCH_ENABLE=1`` forces the pch on.
ESP-IDF runs the same check in CMake (build_gen.espidf)."""
return not pch_forced() and ccache_breaks_pch()
def pch_usable(cxx: Sequence[Path | str]) -> bool:
"""The knob plus the ccache and host rules; ``ESPHOME_PCH_ENABLE=1``
skips the rules."""
if not pch_enabled() or _ccache_rules_out_pch():
"""The knob plus the host rule; ``ESPHOME_PCH_ENABLE=1`` skips the rule."""
if not pch_enabled():
return False
if not pch_needs_gcc_check():
return True
@@ -173,11 +162,7 @@ def pch_script_enabled() -> bool:
"""Whether this PlatformIO build takes the pch script."""
from esphome.core import CORE
return (
pch_enabled()
and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
and not _ccache_rules_out_pch()
)
return pch_enabled() and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
def pch_header_text(include_headers: Iterable[str]) -> str:
+1 -27
View File
@@ -33,35 +33,9 @@ _CACHE_VERSION = 1
_LAMBDA_KEY = "__esphome_lambda__"
_CACHE_SUFFIX = ".validated.json"
def compiled_config_path(config_filename: str) -> Path:
"""Path to the cached validated config alongside the storage sidecar."""
return CORE.data_dir / "storage" / f"{config_filename}{_CACHE_SUFFIX}"
def invalidate_compiled_config() -> None:
"""Drop every cache in the storage dir after a change to a file the
mtime check cannot see, such as secrets.yaml or an include; other
configurations may share that file."""
storage = compiled_config_path(CORE.config_filename).parent
try:
caches = [p for p in storage.iterdir() if p.name.endswith(_CACHE_SUFFIX)]
except FileNotFoundError:
return
except OSError as err:
raise EsphomeError(
f"Could not list the validated config caches in {storage}: {err}"
) from err
for path in caches:
try:
path.unlink(missing_ok=True)
except OSError as err:
# A stale cache would present the old key on the next upload
raise EsphomeError(
f"Could not remove the validated config cache {path}: {err}"
) from err
return CORE.data_dir / "storage" / f"{config_filename}.validated.json"
def save_compiled_config(config: ConfigType) -> None:
+3 -1
View File
@@ -297,7 +297,9 @@ void ADE7880::init_device_() {
void ADE7880::reset_device_() {
if (this->reset_pin_ != nullptr) {
ESP_LOGD(TAG, "Reset device using RESET pin");
pulse_reset(this->reset_pin_, {.low_ms = 1});
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
} else {
ESP_LOGD(TAG, "Reset device using SWRST command");
this->write_u16_register16_(CONFIG, CONFIG_SWRST);
+73 -73
View File
@@ -14,80 +14,80 @@ ESPHOME_LOG_TAG(TAG, "aic3204");
return; \
}
static constexpr i2c::I2CRegValue AIC3204_INIT[] PROGMEM = {
// Set register page to 0
{AIC3204_PAGE_CTRL, 0x00},
// Initiate SW reset (PLL is powered off as part of reset)
{AIC3204_SW_RST, 0x01},
// *** Program clock settings ***
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
// for determining these settings.
// Power up NDAC and set to 2
{AIC3204_NDAC, 0x82},
// Power up MDAC and set to 2
{AIC3204_MDAC, 0x82},
// Program DOSR = 128
{AIC3204_DOSR, 0x80},
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
{AIC3204_CODEC_IF, 0x30},
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
{AIC3204_SCLK_MFP3, 0x02},
{AIC3204_AUDIO_IF_4, 0x01},
{AIC3204_AUDIO_IF_5, 0x01},
// Program the DAC processing block to be used - PRB_P1
{AIC3204_DAC_SIG_PROC, 0x01},
// *** Select Page 1 ***
{AIC3204_PAGE_CTRL, 0x01},
// Enable the internal AVDD_LDO:
{AIC3204_LDO_CTRL, 0x09},
// *** Program Analog Blocks ***
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
{AIC3204_PWR_CFG, 0x08},
// Enable Master Analog Power Control
{AIC3204_LDO_CTRL, 0x01},
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
{AIC3204_CM_CTRL, 0x40},
// *** Set PowerTune Modes ***
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
{AIC3204_PLAY_CFG1, 0x00},
{AIC3204_PLAY_CFG2, 0x00},
// Set the REF charging time to 40ms
{AIC3204_REF_STARTUP, 0x01},
// HP soft stepping settings for optimal pop performance at power up
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
{AIC3204_HP_START, 0x25},
// Route Left DAC to HPL
{AIC3204_HPL_ROUTE, 0x08},
// Route Right DAC to HPR
{AIC3204_HPR_ROUTE, 0x08},
// Route Left DAC to LOL
{AIC3204_LOL_ROUTE, 0x08},
// Route Right DAC to LOR
{AIC3204_LOR_ROUTE, 0x08},
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
{AIC3204_HPL_GAIN, 0x3e},
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
{AIC3204_HPR_GAIN, 0x3e},
// Unmute LOL and set gain to 0dB
{AIC3204_LOL_DRV_GAIN, 0x00},
// Unmute LOR and set gain to 0dB
{AIC3204_LOR_DRV_GAIN, 0x00},
// Power up HPL and HPR, LOL and LOR drivers
{AIC3204_OP_PWR_CTRL, 0x3C},
};
void AIC3204::setup() {
ERROR_CHECK(this->write_registers(AIC3204_INIT), "Failed to initialize");
// Set register page to 0
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
// Initiate SW reset (PLL is powered off as part of reset)
ERROR_CHECK(this->write_byte(AIC3204_SW_RST, 0x01), "Software reset failed");
// *** Program clock settings ***
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
// for determining these settings.
// Power up NDAC and set to 2
ERROR_CHECK(this->write_byte(AIC3204_NDAC, 0x82), "Set NDAC failed");
// Power up MDAC and set to 2
ERROR_CHECK(this->write_byte(AIC3204_MDAC, 0x82), "Set MDAC failed");
// Program DOSR = 128
ERROR_CHECK(this->write_byte(AIC3204_DOSR, 0x80), "Set DOSR failed");
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
ERROR_CHECK(this->write_byte(AIC3204_CODEC_IF, 0x30), "Set CODEC_IF failed");
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
ERROR_CHECK(this->write_byte(AIC3204_SCLK_MFP3, 0x02), "Set SCLK/MFP3 failed");
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_4, 0x01), "Set AUDIO_IF_4 failed");
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_5, 0x01), "Set AUDIO_IF_5 failed");
// Program the DAC processing block to be used - PRB_P1
ERROR_CHECK(this->write_byte(AIC3204_DAC_SIG_PROC, 0x01), "Set DAC_SIG_PROC failed");
// *** Select Page 1 ***
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x01), "Set page 1 failed");
// Enable the internal AVDD_LDO:
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x09), "Set LDO_CTRL failed");
// *** Program Analog Blocks ***
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
ERROR_CHECK(this->write_byte(AIC3204_PWR_CFG, 0x08), "Set PWR_CFG failed");
// Enable Master Analog Power Control
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x01), "Set LDO_CTRL failed");
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
ERROR_CHECK(this->write_byte(AIC3204_CM_CTRL, 0x40), "Set CM_CTRL failed");
// *** Set PowerTune Modes ***
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG1, 0x00), "Set PLAY_CFG1 failed");
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG2, 0x00), "Set PLAY_CFG2 failed");
// Set the REF charging time to 40ms
ERROR_CHECK(this->write_byte(AIC3204_REF_STARTUP, 0x01), "Set REF_STARTUP failed");
// HP soft stepping settings for optimal pop performance at power up
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
ERROR_CHECK(this->write_byte(AIC3204_HP_START, 0x25), "Set HP_START failed");
// Route Left DAC to HPL
ERROR_CHECK(this->write_byte(AIC3204_HPL_ROUTE, 0x08), "Set HPL_ROUTE failed");
// Route Right DAC to HPR
ERROR_CHECK(this->write_byte(AIC3204_HPR_ROUTE, 0x08), "Set HPR_ROUTE failed");
// Route Left DAC to LOL
ERROR_CHECK(this->write_byte(AIC3204_LOL_ROUTE, 0x08), "Set LOL_ROUTE failed");
// Route Right DAC to LOR
ERROR_CHECK(this->write_byte(AIC3204_LOR_ROUTE, 0x08), "Set LOR_ROUTE failed");
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
ERROR_CHECK(this->write_byte(AIC3204_HPL_GAIN, 0x3e), "Set HPL_GAIN failed");
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
ERROR_CHECK(this->write_byte(AIC3204_HPR_GAIN, 0x3e), "Set HPR_GAIN failed");
// Unmute LOL and set gain to 0dB
ERROR_CHECK(this->write_byte(AIC3204_LOL_DRV_GAIN, 0x00), "Set LOL_DRV_GAIN failed");
// Unmute LOR and set gain to 0dB
ERROR_CHECK(this->write_byte(AIC3204_LOR_DRV_GAIN, 0x00), "Set LOR_DRV_GAIN failed");
// Power up HPL and HPR, LOL and LOR drivers
ERROR_CHECK(this->write_byte(AIC3204_OP_PWR_CTRL, 0x3C), "Set OP_PWR_CTRL failed");
// Wait for 2.5 sec for soft stepping to take effect before attempting power-up
this->set_timeout(2500, [this]() {
-4
View File
@@ -90,10 +90,6 @@ class AirtonClimate : public climate_ir::ClimateIR {
bool set_display_state(bool state);
bool set_vertical_direction_state(VerticalDirection direction);
void send_settings();
void control_vertical_direction(VerticalDirection direction) {
if (this->set_vertical_direction_state(direction))
this->send_settings();
}
void set_setting(AirtonSetting setting, bool state) {
const bool changed = setting == AirtonSetting::AIRTON_SETTING_DISPLAY ? this->set_display_state(state)
: this->set_sleep_mode_state(state);
+4 -4
View File
@@ -9,16 +9,18 @@ from ..climate import (
CONF_VERTICAL_DIRECTION,
VERTICAL_DIRECTIONS,
AirtonClimate,
VerticalDirection,
airton_ns,
)
CODEOWNERS = ["@Procsiab"]
VerticalDirectionSelect = airton_ns.class_("VerticalDirectionSelect", select.Select)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_AIRTON_ID): cv.use_id(AirtonClimate),
cv.Optional(CONF_VERTICAL_DIRECTION): select.select_schema(
select.ApplySelect,
VerticalDirectionSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
@@ -32,6 +34,4 @@ async def to_code(config: ConfigType) -> None:
CONF_VERTICAL_DIRECTION,
hub.set_vertical_direction_select,
options=list(VERTICAL_DIRECTIONS),
# The options are in the order of the enum; the hub publishes the state
apply=f"control_vertical_direction(static_cast<{VerticalDirection}>(index))",
)
@@ -0,0 +1,17 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../airton.h"
namespace esphome::airton {
class VerticalDirectionSelect : public select::Select, public Parented<AirtonClimate> {
protected:
// The options are in the order of the enum, so the index is the direction; the parent publishes the state
void control(size_t index) override {
if (this->parent_->set_vertical_direction_state(static_cast<VerticalDirection>(index)))
this->parent_->send_settings();
}
};
} // namespace esphome::airton
@@ -1,6 +1,7 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_CODE,
@@ -9,11 +10,11 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_STATE,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
)
@@ -43,12 +44,12 @@ AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
_ALARM_CONTROL_PANEL_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTAlarmControlPanelComponent")
mqtt.MQTTAlarmControlPanelComponent
),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}),
@@ -165,7 +166,11 @@ _CALLBACK_AUTOMATIONS = (
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
await entity_helpers.setup_entity_integrations(var, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
async def register_alarm_control_panel(var, config):
+73 -62
View File
@@ -11,52 +11,7 @@ ESPHOME_LOG_TAG(TAG, "apds9960");
this->mark_failed(LOG_STR("Setting up APDS9960 registers failed")); \
return; \
}
static constexpr i2c::I2CRegValue APDS9960_TIMING[] PROGMEM = {
// ATime (ADC integration time, 2.78ms increments, 0x81) -> 0xDB (103ms)
{0x81, 0xDB},
// WTime (Wait time, 0x83) -> 0xF6 (27ms)
{0x83, 0xF6},
// PPulse (0x8E) -> 0x87 (16us, 8 pulses)
{0x8E, 0x87},
// POffset UR (0x9D) -> 0 (no offset)
{0x9D, 0x00},
// POffset DL (0x9E) -> 0 (no offset)
{0x9E, 0x00},
// Config 1 (0x8D) -> 0x60 (no wtime factor)
{0x8D, 0x60},
};
static constexpr i2c::I2CRegValue APDS9960_INTERRUPT_GESTURE[] PROGMEM = {
// Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt)
{0x8C, 0x11},
// Config 2 (0x90) -> 0x01 (no saturation interrupts or LED boost)
{0x90, 0x01},
// Config 3 (0x9F) -> 0x00 (enable all photodiodes, no SAI)
{0x9F, 0x00},
// GPenTh (0xA0, gesture enter threshold) -> 0x28 (also 0x32)
{0xA0, 0x28},
// GPexTh (0xA1, gesture exit threshold) -> 0x1E
{0xA1, 0x1E},
// GConf 1 (0xA2, gesture config 1) -> 0x40 (4 gesture events for interrupt (GFIFO 3), 1 for exit)
{0xA2, 0x40},
};
static constexpr i2c::I2CRegValue APDS9960_GESTURE_OFFSETS[] PROGMEM = {
// GOffsetU (0xA4) -> 0x00 (no offset)
{0xA4, 0x00},
// GOffsetD (0xA5) -> 0x00 (no offset)
{0xA5, 0x00},
// GOffsetL (0xA7) -> 0x00 (no offset)
{0xA7, 0x00},
// GOffsetR (0xA9) -> 0x00 (no offset)
{0xA9, 0x00},
// GPulse (0xA6) -> 0xC9 (32 µs, 10 pulses)
{0xA6, 0xC9},
// GConf 3 (0xAA, gesture config 3) -> 0x00 (all photodiodes active during gesture, all gesture dimensions enabled)
// 0x00 -> all dimensions, 0x01 -> up down, 0x02 -> left right
{0xAA, 0x00},
};
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
void APDS9960::setup() {
uint8_t id;
@@ -70,27 +25,83 @@ void APDS9960::setup() {
return;
}
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_TIMING));
// ATime (ADC integration time, 2.78ms increments, 0x81) -> 0xDB (103ms)
APDS9960_WRITE_BYTE(0x81, 0xDB);
// WTime (Wait time, 0x83) -> 0xF6 (27ms)
APDS9960_WRITE_BYTE(0x83, 0xF6);
// PPulse (0x8E) -> 0x87 (16us, 8 pulses)
APDS9960_WRITE_BYTE(0x8E, 0x87);
// POffset UR (0x9D) -> 0 (no offset)
APDS9960_WRITE_BYTE(0x9D, 0x00);
// POffset DL (0x9E) -> 0 (no offset)
APDS9960_WRITE_BYTE(0x9E, 0x00);
// Config 1 (0x8D) -> 0x60 (no wtime factor)
APDS9960_WRITE_BYTE(0x8D, 0x60);
// Control (0x8F): led drive (0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA),
// proximity gain (0 -> 1x, 1 -> 2X, 2 -> 4X, 3 -> 8X), ambient light gain (0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x)
APDS9960_ERROR_CHECK(this->update_register(
0x8F, 0b11001111,
(this->led_drive_ & 0b11) << 6 | (this->proximity_gain_ & 0b11) << 2 | (this->ambient_gain_ & 0b11)));
// Control (0x8F) ->
uint8_t val = 0;
APDS9960_ERROR_CHECK(this->read_byte(0x8F, &val));
val &= 0b00111111;
// led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
val |= (this->led_drive_ & 0b11) << 6;
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_INTERRUPT_GESTURE));
val &= 0b11110011;
// proximity gain, 0 -> 1x, 1 -> 2X, 2 -> 4X, 3 -> 8X
val |= (this->proximity_gain_ & 0b11) << 2;
// GConf 2 (0xA3): gesture gain (0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x),
// gesture led drive (0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA), gesture wait time
// (0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms, 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms)
APDS9960_ERROR_CHECK(this->update_register(
0xA3, 0b01111111,
(this->gesture_gain_ & 0b11) << 5 | (this->gesture_led_drive_ & 0b11) << 3 | (this->gesture_wait_time_ & 0b111)));
val &= 0b11111100;
// ambient light gain, 0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x
val |= (this->ambient_gain_ & 0b11) << 0;
APDS9960_WRITE_BYTE(0x8F, val);
APDS9960_ERROR_CHECK(this->write_registers(APDS9960_GESTURE_OFFSETS));
// Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt)
APDS9960_WRITE_BYTE(0x8C, 0x11);
// Config 2 (0x90) -> 0x01 (no saturation interrupts or LED boost)
APDS9960_WRITE_BYTE(0x90, 0x01);
// Config 3 (0x9F) -> 0x00 (enable all photodiodes, no SAI)
APDS9960_WRITE_BYTE(0x9F, 0x00);
// GPenTh (0xA0, gesture enter threshold) -> 0x28 (also 0x32)
APDS9960_WRITE_BYTE(0xA0, 0x28);
// GPexTh (0xA1, gesture exit threshold) -> 0x1E
APDS9960_WRITE_BYTE(0xA1, 0x1E);
// GConf 1 (0xA2, gesture config 1) -> 0x40 (4 gesture events for interrupt (GFIFO 3), 1 for exit)
APDS9960_WRITE_BYTE(0xA2, 0x40);
// GConf 2 (0xA3, gesture config 2) ->
APDS9960_ERROR_CHECK(this->read_byte(0xA3, &val));
val &= 0b10011111;
// gesture gain, 0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x
val |= (this->gesture_gain_ & 0b11) << 5;
val &= 0b11100111;
// gesture led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
val |= (this->gesture_led_drive_ & 0b11) << 3;
val &= 0b11111000;
// gesture wait time
// 0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms
// 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms
val |= (this->gesture_wait_time_ & 0b111) << 0;
APDS9960_WRITE_BYTE(0xA3, val);
// GOffsetU (0xA4) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA4, 0x00);
// GOffsetD (0xA5) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA5, 0x00);
// GOffsetL (0xA7) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA7, 0x00);
// GOffsetR (0xA9) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA9, 0x00);
// GPulse (0xA6) -> 0xC9 (32 µs, 10 pulses)
APDS9960_WRITE_BYTE(0xA6, 0xC9);
// GConf 3 (0xAA, gesture config 3) -> 0x00 (all photodiodes active during gesture, all gesture dimensions enabled)
// 0x00 -> all dimensions, 0x01 -> up down, 0x02 -> left right
APDS9960_WRITE_BYTE(0xAA, 0x00);
// Enable (0x80) ->
uint8_t val = 0;
val = 0;
val |= (0b1) << 0; // power on
val |= (this->is_color_enabled_() & 0b1) << 1;
val |= (this->is_proximity_enabled_() & 0b1) << 2;
@@ -98,7 +109,7 @@ void APDS9960::setup() {
val |= 0b0 << 4; // color interrupt disabled
val |= 0b0 << 5; // proximity interrupt disabled
val |= (this->is_gesture_enabled_() & 0b1) << 6; // proximity is required for gestures
APDS9960_ERROR_CHECK(this->write_byte(0x80, val));
APDS9960_WRITE_BYTE(0x80, val);
}
bool APDS9960::is_color_enabled_() const {
#ifdef USE_SENSOR
+8 -4
View File
@@ -174,10 +174,14 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
CONF_SUPPORTS_RESPONSE = "supports_response"
SupportsResponseType = api_ns.namespace("enums").enum("SupportsResponseType")
SUPPORTS_RESPONSE_OPTIONS = SupportsResponseType.options(
"none", "optional", "only", "status", prefix="SUPPORTS_RESPONSE_"
)
# Enum values in api::enums namespace
enums_ns = api_ns.namespace("enums")
SUPPORTS_RESPONSE_OPTIONS = {
"none": enums_ns.SUPPORTS_RESPONSE_NONE,
"optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL,
"only": enums_ns.SUPPORTS_RESPONSE_ONLY,
"status": enums_ns.SUPPORTS_RESPONSE_STATUS,
}
def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
+2 -11
View File
@@ -1555,17 +1555,8 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()
void APIConnection::send_event(event::Event *event) {
const uint8_t type_index = event->get_last_event_type_index();
const size_t size_before = this->deferred_batch_.size();
if (this->deferred_batch_.contains(event, EventResponse::MESSAGE_TYPE)) {
// Sending now would overtake the older event of this entity that still waits in the batch
this->schedule_message_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE, type_index);
} else {
this->send_message_smart_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE, type_index);
}
// Events are rare, so a queued one sends the batch now rather than after batch_delay
if (this->deferred_batch_.size() > size_before)
this->make_batch_due_now_();
this->send_message_smart_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE,
event->get_last_event_type_index());
}
uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
uint32_t remaining_size) {
+68 -5
View File
@@ -2,7 +2,6 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_deferred_batch.h"
#include "api_frame_helper.h"
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
@@ -21,6 +20,7 @@
#include "esphome/core/string_ref.h"
#include <functional>
#include <limits>
#include <vector>
namespace esphome {
@@ -656,6 +656,73 @@ class APIConnection final : public APIServerConnectionBase {
// Function pointer type for message encoding
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size);
// Generic batching mechanism for both state updates and entity info
struct DeferredBatch {
// Sentinel value for unused aux_data_index
static constexpr uint8_t AUX_DATA_UNUSED = std::numeric_limits<uint8_t>::max();
struct BatchItem {
EntityBase *entity; // 4 bytes - Entity pointer
uint16_t message_type; // 2 bytes - Message type for protocol and dispatch
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
};
std::vector<BatchItem> items;
uint32_t batch_start_time{0};
// No pre-allocation - log connections never use batching, and for
// connections that do, buffers are released after initial sync anyway
// Add item to the batch (with deduplication)
void add_item(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
uint8_t aux_data_index = AUX_DATA_UNUSED) {
// Dedup: O(n) scan but optimized for RAM over performance
// Skip deduplication for events - they are edge-triggered, every occurrence matters
#ifdef USE_EVENT
if (message_type != EventResponse::MESSAGE_TYPE)
#endif
{
for (const auto &item : this->items) {
if (item.entity == entity && item.message_type == message_type)
return; // Already queued
}
}
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
}
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, uint16_t message_type, uint8_t estimated_size) {
// Swap to front avoids expensive vector::insert which shifts all elements
this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
if (this->items.size() > 1) {
std::swap(this->items.front(), this->items.back());
}
}
// Clear all items
void clear() {
items.clear();
batch_start_time = 0;
}
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; }
// Release excess capacity - only releases if items already empty
void release_buffer() {
// Safe to call: batch is processed before release_buffer is called,
// and if any items remain (partial processing), we must not clear them.
// Use swap trick since shrink_to_fit() is non-binding and may be ignored.
if (items.empty()) {
std::vector<BatchItem>().swap(items);
}
}
};
// DeferredBatch here (16 bytes, 4-byte aligned)
DeferredBatch deferred_batch_;
@@ -728,10 +795,6 @@ class APIConnection final : public APIServerConnectionBase {
static constexpr size_t MAX_BATCH_PACKET_SIZE = 1390; // MTU
bool schedule_batch_();
// Sends the batch on the next loop, so a queued event does not wait for batch_delay; queued states go with it
void make_batch_due_now_() {
this->deferred_batch_.batch_start_time = App.get_loop_component_start_time() - this->get_batch_delay_ms_();
}
void process_batch_();
void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
__attribute__((noinline));
@@ -1,88 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_pb2.h"
#include "esphome/core/entity_base.h"
#include <algorithm>
#include <cstdint>
#include <limits>
#include <vector>
namespace esphome::api {
// Generic batching mechanism for both state updates and entity info
struct DeferredBatch {
// Sentinel value for unused aux_data_index
static constexpr uint8_t AUX_DATA_UNUSED = std::numeric_limits<uint8_t>::max();
struct BatchItem {
EntityBase *entity; // 4 bytes - Entity pointer
uint16_t message_type; // 2 bytes - Message type for protocol and dispatch
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
};
std::vector<BatchItem> items;
uint32_t batch_start_time{0};
// No pre-allocation - log connections never use batching, and for
// connections that do, buffers are released after initial sync anyway
// O(n) scan but optimized for RAM over performance
bool contains(EntityBase *entity, uint16_t message_type) const {
for (const auto &item : this->items) {
if (item.entity == entity && item.message_type == message_type)
return true;
}
return false;
}
// Add item to the batch (with deduplication)
void add_item(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
uint8_t aux_data_index = AUX_DATA_UNUSED) {
// Skip deduplication for events - they are edge-triggered, every occurrence matters
#ifdef USE_EVENT
if (message_type != EventResponse::MESSAGE_TYPE)
#endif
{
if (this->contains(entity, message_type))
return; // Already queued
}
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
}
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, uint16_t message_type, uint8_t estimated_size) {
// Shift rather than swap, so the queued items keep their order (two events of one entity must not swap)
this->items.push_back({});
std::move_backward(this->items.begin(), this->items.end() - 1, this->items.end());
this->items.front() = {entity, message_type, estimated_size, AUX_DATA_UNUSED};
}
// Clear all items
void clear() {
items.clear();
batch_start_time = 0;
}
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; }
// Release excess capacity - only releases if items already empty
void release_buffer() {
// Safe to call: batch is processed before release_buffer is called,
// and if any items remain (partial processing), we must not clear them.
// Use swap trick since shrink_to_fit() is non-binding and may be ignored.
if (items.empty()) {
std::vector<BatchItem>().swap(items);
}
}
};
} // namespace esphome::api
#endif // USE_API
+13 -5
View File
@@ -6,11 +6,19 @@ namespace esphome::as3935_i2c {
ESPHOME_LOG_TAG(TAG, "as3935_i2c");
void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) {
const uint8_t value = bits << start_pos;
// Bits of value outside mask were ORed in before, so they stay part of the update
// Also called at runtime, so a transient bus error only warns, like read_register()
if (!this->update_register(reg, mask | value, value)) {
ESP_LOGW(TAG, "Failed to update register 0x%02X", reg);
uint8_t write_reg;
if (!this->read_byte(reg, &write_reg)) {
this->mark_failed();
ESP_LOGW(TAG, "read_byte failed - increase log level for more details!");
return;
}
write_reg &= (~mask);
write_reg |= (bits << start_pos);
if (!this->write_byte(reg, write_reg)) {
ESP_LOGW(TAG, "write_byte failed - increase log level for more details!");
return;
}
}
+8 -2
View File
@@ -240,11 +240,17 @@ bool AS7341Component::write_register_bit(uint8_t address, bool value, uint8_t bi
}
bool AS7341Component::set_register_bit(uint8_t address, uint8_t bit_position) {
return this->update_register(address, 1 << bit_position, 1 << bit_position);
uint8_t data;
this->read_byte(address, &data);
data |= (1 << bit_position);
return this->write_byte(address, data);
}
bool AS7341Component::clear_register_bit(uint8_t address, uint8_t bit_position) {
return this->update_register(address, 1 << bit_position, 0);
uint8_t data;
this->read_byte(address, &data);
data &= ~(1 << bit_position);
return this->write_byte(address, data);
}
} // namespace esphome::as7341
+12 -8
View File
@@ -54,8 +54,7 @@ static const char AS7341_NAME[] PROGMEM = "AS7341";
static constexpr uint8_t AS7341_ADC_SLOTS = 6;
static constexpr uint8_t AS7341_SMUX_LEN = 20;
static constexpr uint8_t AS7341_SMUX_STEPS = 2;
static constexpr i2c::I2CRegValue AS7341_INIT[] PROGMEM = {
{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
static const RegValue AS7341_INIT[] PROGMEM = {{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
static const uint8_t AS7341_SMUX[] PROGMEM = {
0x30, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x50, 0x00, // step 0: F1 to F4, NIR, Clear
0x00, 0x00, 0x20, 0x04, 0x00, 0x30, 0x01, 0x50, 0x00, 0x06, // step 0, continued
@@ -151,8 +150,8 @@ static constexpr uint8_t AS7343_ADC_SLOTS = 18;
static constexpr uint8_t AS7343_SMUX_LEN = 10;
static const char TCS3448_NAME[] PROGMEM = "TCS3448";
// CFG6, FD_CFG0, CFG10, CFG8, CFG20 (auto_smux, 18 channels in 3 cycles), AGC_GAIN_MAX, FD_TIME_1, FD_TIME_2
static constexpr i2c::I2CRegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
static const RegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
static const uint8_t AS7343_SMUX[] PROGMEM = {0x00, 0x04, 0x65, 0x02, 0x00, 0x05, 0x00, 0x01, 0x00, 0x30,
0x00, 0x00, 0x60, 0x20, 0x04, 0x50, 0x03, 0x00, 0x01, 0x05,
0x05, 0x00, 0x60, 0x00, 0x30, 0x00, 0x40, 0x10, 0x20, 0x00};
@@ -231,7 +230,7 @@ static constexpr ChipInfo as7343_chip(const char *name) {
.gain_correction = AS7343_GAIN_CORRECTION,
.contributions = AS7343_CONTRIBUTIONS,
.tristimulus = AS7343_TRISTIMULUS,
.init_count = sizeof(AS7343_INIT) / sizeof(i2c::I2CRegValue),
.init_count = sizeof(AS7343_INIT) / sizeof(RegValue),
.astep_reg = 0xD4,
.cfg0_reg = 0xBF,
.cfg1_reg = 0xC6,
@@ -265,7 +264,12 @@ bool AS734XComponent::select_bank_(uint8_t reg) {
}
bool AS734XComponent::update_bits_(uint8_t reg, uint8_t mask, bool value) {
return this->select_bank_(reg) && this->update_register(reg, mask, value ? mask : 0);
uint8_t data = 0;
if (!this->select_bank_(reg) || !this->read_byte(reg, &data)) {
return false;
}
data = value ? (data | mask) : (data & ~mask);
return this->write_byte(reg, data);
}
// The exposure is applied before every measurement, so a gain, ATIME or ASTEP changed at runtime
@@ -299,9 +303,9 @@ void AS734XComponent::setup() {
this->mark_failed(LOG_STR("Wrong chip ID"));
return;
}
const i2c::I2CRegValue *init = pgm(chip->init);
const RegValue *init = pgm(chip->init);
for (uint8_t i = 0, n = pgm(chip->init_count); ok && i < n; i++) {
i2c::I2CRegValue rv;
RegValue rv;
progmem_memcpy(&rv, &init[i], sizeof(rv));
ok = this->select_bank_(rv.reg) && this->write_byte(rv.reg, rv.value);
}
+6 -1
View File
@@ -68,10 +68,15 @@ struct ChannelTristimulus {
float z;
};
struct RegValue {
uint8_t reg;
uint8_t value;
};
/// Per-chip constants, stored in flash. The pointed-to tables are in flash too.
struct ChipInfo {
const char *name;
const i2c::I2CRegValue *init; // written in order; the bank is selected per register address
const RegValue *init; // written in order; the bank is selected per register address
const uint8_t *smux; // AS7341: SMUX RAM image per step; AS7343: the three chain images
const uint8_t *readout; // per step, the ADC slot of each band (0xFF: not read in this step)
const uint16_t *wavelengths; // per band, WIDEBAND_NM for clear and ABSENT_NM for a missing band
+2 -1
View File
@@ -108,7 +108,8 @@ CONFIG_SCHEMA = cv.Schema(
def _final_validate(config: ConfigType) -> ConfigType:
hub = fv.get_declaring_config(config[CONF_AS734X_ID])
full = fv.full_config.get()
hub = full.get_config_for_path(full.get_path_for_id(config[CONF_AS734X_ID])[:-1])
if hub[CONF_TYPE] != "AS7341":
return config
for key in _BAND_KEYS:
+7 -6
View File
@@ -4,13 +4,14 @@ import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI
from esphome.types import ConfigType
from .. import CONF_AT581X_ID, AT581XComponent
from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns
DEPENDENCIES = ["at581x"]
RFSwitch = at581x_ns.class_("RFSwitch", switch.Switch)
CONFIG_SCHEMA = switch.switch_schema(
switch.ApplySwitch,
RFSwitch,
device_class=DEVICE_CLASS_SWITCH,
icon=ICON_WIFI,
).extend(
@@ -23,7 +24,7 @@ CONFIG_SCHEMA = switch.switch_schema(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_AT581X_ID])
await switch.sub_switches(config, parent=hub).create(
config, hub.set_rf_power_switch, apply="set_rf_mode(state)", publish="before"
)
at581x_component = await cg.get_variable(config[CONF_AT581X_ID])
s = await switch.new_switch(config)
await cg.register_parented(s, config[CONF_AT581X_ID])
cg.add(at581x_component.set_rf_power_switch(s))
@@ -0,0 +1,10 @@
#include "rf_switch.h"
namespace esphome::at581x {
void RFSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_rf_mode(state);
}
} // namespace esphome::at581x
@@ -0,0 +1,13 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../at581x.h"
namespace esphome::at581x {
class RFSwitch final : public switch_::Switch, public Parented<AT581XComponent> {
protected:
void write_state(bool state) override;
};
} // namespace esphome::at581x
@@ -58,7 +58,7 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const ble_device_base::Ser
return {};
}
const auto &raw = service_data.data;
auto raw = service_data.data;
if (raw.size() < 13) {
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
return {};
@@ -16,7 +16,13 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a,
}
void AXS15231Touchscreen::setup() {
pulse_reset(this->reset_pin_, {.low_ms = 5, .high_after_ms = 10});
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
delay(10);
}
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
+47 -49
View File
@@ -4,9 +4,7 @@
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/core/application.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
namespace esphome::bedjet {
@@ -42,7 +40,7 @@ void BedJetHub::upgrade_firmware() {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name().c_str(), status);
}
}
@@ -66,7 +64,7 @@ bool BedJetHub::set_fan_index(uint8_t fan_speed_index) {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
@@ -84,7 +82,7 @@ bool BedJetHub::set_target_temp(float temp_c) {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
@@ -95,7 +93,7 @@ bool BedJetHub::set_time_remaining(uint8_t hours, uint8_t mins) {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name().c_str(), status);
}
return status == 0;
}
@@ -105,10 +103,11 @@ bool BedJetHub::send_button(BedjetButton button) {
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name(),
ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name().c_str(),
LOG_STR_ARG(bedjet_button_to_string(button)), status);
} else {
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name(), LOG_STR_ARG(bedjet_button_to_string(button)));
ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name().c_str(),
LOG_STR_ARG(bedjet_button_to_string(button)));
}
return status == 0;
}
@@ -126,9 +125,9 @@ uint16_t BedJetHub::get_time_remaining() {
uint8_t BedJetHub::write_bedjet_packet_(BedjetPacket *pkt) {
if (!this->is_connected()) {
if (!this->parent_->enabled) {
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name());
ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str());
} else {
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name());
ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
}
return -1;
}
@@ -145,16 +144,16 @@ uint8_t BedJetHub::set_notify_(const bool enable) {
status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
this->char_handle_status_);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
}
} else {
status = esp_ble_gattc_unregister_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
this->char_handle_status_);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
}
}
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name(), enable, status);
ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
return status;
}
@@ -165,7 +164,7 @@ bool BedJetHub::discover_characteristics_() {
if (!this->char_handle_cmd_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name());
ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_cmd_ = chr->handle;
@@ -175,7 +174,7 @@ bool BedJetHub::discover_characteristics_() {
if (!this->char_handle_status_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name());
ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_status_ = chr->handle;
@@ -205,14 +204,14 @@ bool BedJetHub::discover_characteristics_() {
if (!this->char_handle_name_) {
chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name());
ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name().c_str());
result = false;
} else {
this->char_handle_name_ = chr->handle;
auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
this->char_handle_name_, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name(), status);
ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
}
}
}
@@ -223,7 +222,7 @@ bool BedJetHub::discover_characteristics_() {
" - Status char: 0x%x\n"
" - config descriptor: 0x%x\n"
" - Name char: 0x%x",
this->get_name(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
this->get_name().c_str(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
this->char_handle_name_);
return result;
@@ -242,7 +241,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
auto result = this->discover_characteristics_();
if (result) {
ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name());
ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name().c_str());
this->node_state = espbt::ClientState::ESTABLISHED;
this->set_notify_(true);
@@ -254,7 +253,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
this->dispatch_state_(true);
} else {
ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name());
ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name().c_str());
this->parent()->set_enabled(false);
this->status_set_warning();
this->dispatch_state_(false);
@@ -266,15 +265,15 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
if (param->write.status == ESP_GATT_INVALID_ATTR_LEN) {
// This probably means that our hack for notify_en (8 bit vs 16 bit) didn't work right.
// Should we try to fall back to BLEClient's way?
ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name(),
ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
param->write.handle, param->write.status);
} else {
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name(), param->write.handle,
param->write.status);
ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name().c_str(),
param->write.handle, param->write.status);
}
break;
}
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name(), param->write.handle,
ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
param->write.status);
break;
}
@@ -307,11 +306,9 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
} else if (param->read.handle == this->char_handle_name_) {
// The data should represent the name.
if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) {
ESP_LOGV(TAG, "[%s] Got BedJet name: '%.*s'", this->get_name(), static_cast<int>(param->read.value_len),
reinterpret_cast<const char *>(param->read.value));
const size_t len = std::min<size_t>(param->read.value_len, sizeof(this->name_) - 1);
memcpy(this->name_, param->read.value, len);
this->name_[len] = '\0';
std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len);
ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
this->set_name_(bedjet_name);
}
}
break;
@@ -325,8 +322,8 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
// doesn't break anything.
if (param->reg_for_notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", this->get_name(),
param->reg_for_notify.handle, this->char_handle_status_);
ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x",
this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_);
break;
}
@@ -338,8 +335,8 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
// This event is not handled by the parent BLEClient, so we need to do this either way.
if (param->unreg_for_notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", this->get_name(),
param->unreg_for_notify.handle, this->char_handle_status_);
ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x",
this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_);
break;
}
@@ -353,13 +350,13 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
break;
if (param->notify.conn_id != this->parent_->get_conn_id()) {
ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x", this->get_name(),
this->parent_->get_conn_id(), param->notify.conn_id);
ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x",
this->get_name().c_str(), this->parent_->get_conn_id(), param->notify.conn_id);
// FIXME: bug in BLEClient holding wrong conn_id.
}
if (param->notify.handle != this->char_handle_status_) {
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name(),
ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(),
this->char_handle_status_, param->notify.handle);
break;
}
@@ -375,13 +372,13 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
if (!this->force_refresh_ && this->codec_->compare(param->notify.value, param->notify.value_len)) {
// If the packet is meaningfully different, trigger children as well
this->force_refresh_ = true;
ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name());
ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name().c_str());
}
if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
// Set reentrant flag to prevent processing multiple packets.
this->processing_ = true;
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name(),
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name().c_str(),
this->last_notify_, delta, this->force_refresh_ ? "y" : "n");
bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
@@ -391,7 +388,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name());
ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str());
}
} else {
this->status_packet_ready_();
@@ -400,7 +397,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
break;
}
default:
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name(), event);
ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
break;
}
}
@@ -439,7 +436,7 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) {
ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
return status;
}
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name(),
ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name().c_str(),
enable ? "true" : "false", handle, this->parent_->get_conn_id());
return ESP_GATT_OK;
}
@@ -471,7 +468,7 @@ void BedJetHub::setup_time_() {
void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
if (!this->is_connected()) {
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name());
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
return;
}
@@ -480,7 +477,7 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
if (status) {
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
} else {
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name(), hour, minute);
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
}
}
@@ -498,7 +495,7 @@ void BedJetHub::dump_config() {
"BedJet Hub '%s'\n"
" ble_client.app_id: %d\n"
" ble_client.conn_id: %d",
this->get_name(), this->parent()->app_id, this->parent()->get_conn_id());
this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id());
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size());
for (auto *child : this->children_) {
@@ -516,9 +513,10 @@ void BedJetHub::dispatch_status_() {
auto *status = this->codec_->get_status_packet();
if (!this->is_connected()) {
ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name());
ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name().c_str());
} else if (status != nullptr) {
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name(), this->children_.size(), status);
ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name().c_str(), this->children_.size(),
status);
for (auto *child : this->children_) {
child->on_status(status);
}
@@ -532,13 +530,13 @@ void BedJetHub::dispatch_status_() {
// We can try to unregister for notifications now, and then re-register, hoping to clear it up...
// But how do we know for sure which state we're in, and how do we actually clear out the buggy state?
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name());
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
} else if (diff > NOTIFY_WARN_THRESHOLD) {
ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name(), diff / 1000);
ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name().c_str(), diff / 1000);
}
if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying", this->get_name(), this->timeout_);
ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying", this->get_name().c_str(), this->timeout_);
// set_enabled(false) will only close the connection if state != IDLE.
this->parent()->set_state(espbt::ClientState::CONNECTING);
this->parent()->set_enabled(false);
+10 -3
View File
@@ -119,7 +119,13 @@ class BedJetHub final : public esphome::ble_client::BLEClientNode, public Pollin
float get_setup_priority() const override { return setup_priority::BLUETOOTH; }
/** @return The BedJet's configured name, or the MAC address if not discovered yet. */
const char *get_name() const { return this->name_[0] == '\0' ? this->parent_->address_str() : this->name_; }
std::string get_name() {
if (this->name_.empty()) {
return this->parent_->address_str();
} else {
return this->name_;
}
}
/* BLEClient overrides */
@@ -145,8 +151,9 @@ class BedJetHub final : public esphome::ble_client::BLEClientNode, public Pollin
uint8_t set_notify_(bool enable);
/** Send the `BedjetPacket` to the device. */
uint8_t write_bedjet_packet_(BedjetPacket *pkt);
// Only used in log lines; longer names are truncated
char name_[24]{};
void set_name_(const std::string &name) { this->name_ = name; }
std::string name_;
uint32_t last_notify_ = 0;
inline void status_packet_ready_();
@@ -62,13 +62,6 @@ struct SpiData {
static SpiData *spi_data = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// BDK 3.0.78 makes REG_WRITE a statement expression that yields the volatile assignment,
// a deprecated volatile read under clang
static inline void reg_write(uint32_t addr, uint32_t value) {
// NOLINTNEXTLINE(performance-no-int-to-ptr) - MMIO register at a fixed address
*reinterpret_cast<volatile uint32_t *>(addr) = value;
}
static void set_spi_ctrl_register(uint32_t bit, bool val) {
uint32_t value = REG_READ(SPI_CTRL);
if (val == 0) {
@@ -76,7 +69,7 @@ static void set_spi_ctrl_register(uint32_t bit, bool val) {
} else if (val == 1) {
value |= bit;
}
reg_write(SPI_CTRL, value);
REG_WRITE(SPI_CTRL, value);
}
#ifdef BEKEN_SPI_IS_N_LIKE
@@ -87,7 +80,7 @@ static void set_spi_config_register(uint32_t bit, bool val) {
} else if (val == 1) {
value |= bit;
}
reg_write(SPI_CONFIG, value);
REG_WRITE(SPI_CONFIG, value);
}
#endif
@@ -143,7 +136,7 @@ static void spi_set_clock(uint32_t max_hz) {
param = REG_READ(SPI_CTRL);
param &= ~(SPI_CKR_MASK << SPI_CKR_POSI);
param |= (div << SPI_CKR_POSI);
reg_write(SPI_CTRL, param);
REG_WRITE(SPI_CTRL, param);
#ifdef BEKEN_SPI_IS_N_LIKE
const int divisor = div;
#else
@@ -236,7 +229,7 @@ void BekenSPILEDStripLightOutput::setup() {
value = REG_READ(SPI_CTRL);
value &= ~CTRL_NSSMD_3;
value |= (1 << 17);
reg_write(SPI_CTRL, value);
REG_WRITE(SPI_CTRL, value);
#ifdef BEKEN_SPI_IS_N_LIKE
value = GFUNC_MODE_SPI_DMA;
@@ -286,7 +279,7 @@ void BekenSPILEDStripLightOutput::setup() {
value = REG_READ(SPI_CONFIG);
value &= ~(0xFFF << 8);
value |= ((dma_buffer_size & 0xFFF) << 8);
reg_write(SPI_CONFIG, value);
REG_WRITE(SPI_CONFIG, value);
#else
// T-like variants: No transfer-length register. The SPI controller shifts out
// everything the DMA writes into SPI_DAT.
+9 -5
View File
@@ -1,24 +1,28 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import CONF_BH1745_ID, BH1745Component
from .. import CONF_BH1745_ID, BH1745Component, bh1745_ns
DEPENDENCIES = ["bh1745"]
BH1745InterruptPinOutput = bh1745_ns.class_(
"BH1745InterruptPinOutput", output.BinaryOutput
)
CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(output.ApplyBinaryOutput),
cv.GenerateID(): cv.declare_id(BH1745InterruptPinOutput),
cv.GenerateID(CONF_BH1745_ID): cv.use_id(BH1745Component),
}
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
hub = await cg.get_variable(config[CONF_BH1745_ID])
await output.sub_binary_outputs(config, parent=hub).create(
config, None, apply="set_interrupt_state(state)"
)
await cg.register_parented(var, hub)
cg.add(hub.enable_interrupt_output())
@@ -0,0 +1,16 @@
#pragma once
#include "esphome/components/output/binary_output.h"
#include "esphome/core/helpers.h"
#include "../bh1745.h"
namespace esphome::bh1745 {
/// Drives the BH1745 INT pin, which switches the LEDs on breakout boards such as the Pimoroni one.
class BH1745InterruptPinOutput : public output::BinaryOutput, public Parented<BH1745Component> {
protected:
void write_state(bool state) override { this->parent_->set_interrupt_state(state); }
};
} // namespace esphome::bh1745
+4 -2
View File
@@ -63,7 +63,6 @@ from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity
from esphome.core.entity_helpers import (
SubEntities,
entity_duplicate_validator,
lazy_load_setup,
lazy_load_validator,
mqtt_component_class,
queue_entity_register,
@@ -622,7 +621,10 @@ async def setup_binary_sensor_core_(var, config):
await entity_helpers.setup_entity_integrations(var, config)
await lazy_load_setup("zigbee", "setup_binary_sensor", var, config)
if "zigbee" in CORE.loaded_integrations:
from esphome.components import zigbee
await zigbee.setup_binary_sensor(var, config)
async def register_binary_sensor(var, config):
-5
View File
@@ -29,7 +29,6 @@ from esphome.components.libretiny.const import (
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401
from esphome.types import ConfigType
from .boards import BK72XX_BOARD_PINS, BK72XX_BOARDS
@@ -78,7 +77,3 @@ async def pin_to_code(config: ConfigType) -> MockObj:
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
# Called by the OTA upload; the shared libretiny implementation.
ota_image = libretiny.device_ota_image
+26 -54
View File
@@ -21,10 +21,9 @@ public ble_api.h.
"""
import logging
import re
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components import libretiny, wifi
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
@@ -33,30 +32,14 @@ from esphome.components.libretiny.const import (
FAMILY_BK7251,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ENABLE_ON_BOOT,
CONF_ESPHOME,
CONF_ID,
CONF_PLATFORMIO_OPTIONS,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE, EsphomeError
import esphome.final_validate as fv
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
from esphome.core import EsphomeError
from esphome.types import ConfigType
DEPENDENCIES = ["bk72xx"]
CODEOWNERS = ["@Bl00d-B0b"]
DOMAIN = "bk72xx_ble"
BEKEN_BDK_VERSION_OPTION = "custom_versions.beken-bdk"
# Older BDKs lay the BLE stack out differently and can crash at boot or erase the
# bootloader with BLE compiled in; the libretiny platform builds with this one
MIN_BDK_VERSION = (3, 0, 78)
# Older LibreTiny releases arm the Beken MCU sleep with wifi power save, which halts
# the device once the station stops while BLE runs (libretiny-eu/libretiny#414)
MIN_LIBRETINY_VERSION = (1, 14, 0)
_LOGGER = logging.getLogger(__name__)
bk72xx_ble_ns = cg.esphome_ns.namespace("bk72xx_ble")
@@ -97,45 +80,20 @@ def _unsupported_family_message(family: str) -> str | None:
return None
def check_bdk_version(value: str | None) -> None:
"""Reject a beken-bdk override older than the release the BLE stack needs."""
if value is None:
return
# LibreTiny appends the value to the package URL as a git ref and only builds
# 3.0.x releases; the 2021.06.07 tag is the pre 3.0 SDK
match = re.fullmatch(r"v?(\d+)\.(\d+)\.(\d+)", str(value))
if match is not None:
version = tuple(int(g) for g in match.groups())
if version[:2] == MIN_BDK_VERSION[:2] and version >= MIN_BDK_VERSION:
return
minimum = ".".join(str(part) for part in MIN_BDK_VERSION)
raise cv.Invalid(
f"bk72xx_ble needs beken-bdk {minimum} or a newer 3.0.x release, not "
f"'{value}': with BLE compiled in, an older BDK can crash at boot or erase "
"the bootloader"
)
def check_libretiny_version(version: cv.Version) -> None:
"""Reject a LibreTiny release older than the one whose WiFi sleep is safe with BLE."""
if (version.major, version.minor, version.patch) >= MIN_LIBRETINY_VERSION:
return
minimum = ".".join(str(part) for part in MIN_LIBRETINY_VERSION)
raise cv.Invalid(
f"bk72xx_ble needs LibreTiny {minimum} or newer, not {version}: older releases "
"arm the Beken MCU sleep with wifi power save and the device halts once the "
"station stops while BLE runs"
)
def _final_validate(config: ConfigType) -> None:
# Warn only: a hard error here would break the validate-only CI fixtures,
# which run on a BLE 4.2 board. The hard error is raised at codegen.
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
_LOGGER.warning("%s (this configuration cannot compile)", msg)
options = fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS) or {}
check_bdk_version(options.get(BEKEN_BDK_VERSION_OPTION))
check_libretiny_version(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
# Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU
# sleep. With the BLE controller running, that sleep never wakes up once the
# station is stopped (adapter restart after failed roams, wifi.disable): the
# device is dead until a power cycle (esphome#18592). Keep power save off
# until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414).
wifi.force_power_save_off(
"with BLE running, the Beken SDK's MCU sleep halts the device once the "
"station is stopped (https://github.com/esphome/esphome/issues/18592)"
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -158,6 +116,20 @@ async def to_code(config: ConfigType) -> None:
# so this =1 is the single CFG_SUPPORT_BLE define emitted.
cg.add_platformio_option("custom_options.sys_config#h", ["CFG_SUPPORT_BLE=1"])
# Pin the Beken BDK release the BLE 5.x stack is validated against. The
# bundled 3.0.33 has an older BLE header/library layout — and with
# CFG_SUPPORT_BLE=1 the SDK runs its BLE init unconditionally during boot
# (the reason the libretiny platform sets =0 when BLE is unused), so a
# mismatched BDK can crash the device before WiFi comes up regardless of
# enable_on_boot. Pinning here makes a plain config build against the
# validated BDK without any manual platformio_options.
_LOGGER.warning(
"bk72xx_ble builds with beken-bdk 3.0.78 instead of the platform's bundled "
"default: the default's older BLE layout can crash the device at boot when "
"BLE is compiled in"
)
cg.add_platformio_option("custom_versions.beken-bdk", "3.0.78")
# The BDK exposes the controller's BLE address as `common_default_bdaddr` on
# BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is
# derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++
+3 -6
View File
@@ -1,5 +1,4 @@
#include "bl0939.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -21,7 +20,7 @@ static const uint8_t BL0939_REG_SOFT_RESET = 0x19;
static const uint8_t BL0939_REG_USR_WRPROT = 0x1A;
static const uint8_t BL0939_REG_TPS_CTRL = 0x1B;
static constexpr uint8_t BL0939_INIT[6][6] PROGMEM = {
const uint8_t BL0939_INIT[6][6] = {
// Reset to default
{BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33},
// Enable User Operation Write
@@ -67,10 +66,8 @@ void BL0939::update() {
}
void BL0939::setup() {
uint8_t cmd[sizeof(BL0939_INIT[0])];
for (const auto *i : BL0939_INIT) {
progmem_memcpy(cmd, i, sizeof(cmd));
this->write_array(cmd, sizeof(cmd));
for (auto *i : BL0939_INIT) {
this->write_array(i, 6);
delay(1);
}
this->flush();
+30 -5
View File
@@ -139,24 +139,49 @@ bool BME680Component::setup_registers_() {
this->calibration_.ambient_temperature = 25; // prime ambient temperature
// Config register
if (!this->update_register(BME680_REGISTER_CONFIG, 0b00011100, (this->iir_filter_ & 0b111) << 2)) {
uint8_t config_register;
if (!this->read_byte(BME680_REGISTER_CONFIG, &config_register)) {
return false;
}
config_register &= ~0b00011100;
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BME680_REGISTER_CONFIG, config_register)) {
return false;
}
// Humidity control register
if (!this->update_register(BME680_REGISTER_CONTROL_HUMIDITY, 0b00000111, this->humidity_oversampling_ & 0b111)) {
uint8_t hum_control;
if (!this->read_byte(BME680_REGISTER_CONTROL_HUMIDITY, &hum_control)) {
return false;
}
hum_control &= ~0b00000111;
hum_control |= this->humidity_oversampling_ & 0b111;
if (!this->write_byte(BME680_REGISTER_CONTROL_HUMIDITY, hum_control)) {
return false;
}
// Gas 1 control register: run gas, heater profile 0
if (!this->update_register(BME680_REGISTER_CONTROL_GAS1, 0b00011111, 1 << 4)) {
// Gas 1 control register
uint8_t gas1_control;
if (!this->read_byte(BME680_REGISTER_CONTROL_GAS1, &gas1_control)) {
return false;
}
gas1_control &= ~0b00011111;
gas1_control |= 1 << 4;
gas1_control |= 0; // profile 0
if (!this->write_byte(BME680_REGISTER_CONTROL_GAS1, gas1_control)) {
return false;
}
const bool heat_off = this->heater_temperature_ == 0 || this->heater_duration_ == 0;
// Gas 0 control register
if (!this->update_register(BME680_REGISTER_CONTROL_GAS0, 0b00001000, heat_off << 3)) {
uint8_t gas0_control;
if (!this->read_byte(BME680_REGISTER_CONTROL_GAS0, &gas0_control)) {
return false;
}
gas0_control &= ~0b00001000;
gas0_control |= heat_off << 3;
if (!this->write_byte(BME680_REGISTER_CONTROL_GAS0, gas0_control)) {
return false;
}
@@ -1,7 +1,7 @@
#include "bme680_bsec.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <string>
namespace esphome::bme680_bsec {
#ifdef USE_BSEC
@@ -9,7 +9,7 @@ ESPHOME_LOG_TAG(TAG, "bme680_bsec.sensor");
static constexpr uint32_t READ_TIMEOUT_ID = 0;
PROGMEM_STRING_TABLE(IaqAccuracyStrings, "Stabilizing", "Uncertain", "Calibrating", "Calibrated");
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
std::vector<BME680BSECComponent *>
BME680BSECComponent::instances; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -386,11 +386,10 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out
switch (outputs[i].sensor_id) {
case BSEC_OUTPUT_IAQ:
case BSEC_OUTPUT_STATIC_IAQ: {
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_push_([this, accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(accuracy, IaqAccuracyStrings::LAST_INDEX));
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
});
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
@@ -436,9 +435,8 @@ void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, b
sensor->publish_state(value);
}
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value) {
if (!sensor ||
(sensor->has_state() && ESPHOME_strcmp_P(sensor->state.c_str(), reinterpret_cast<ESPHOME_PGM_P>(value)) == 0)) {
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
if (!sensor || (sensor->has_state() && sensor->state == value)) {
return;
}
sensor->publish_state(value);
+1 -1
View File
@@ -77,7 +77,7 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
int64_t get_time_ns_();
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
void publish_sensor_(text_sensor::TextSensor *sensor, ProgmemStr value);
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
void snapshot_state_(); // Fetch the current BSEC library state and save it in the bsec_state_data_ member (volatile
// memory)
+3 -1
View File
@@ -66,7 +66,9 @@ CONFIG_SCHEMA = cv.typed_schema(
def _final_validate(config: ConfigType) -> None:
if config[CONF_TYPE] not in _MAGNETIC_FIELDS:
return
bmi270_config = fv.get_declaring_config(config[CONF_BMI270_ID])
full_config = fv.full_config.get()
bmi270_path = full_config.get_path_for_id(config[CONF_BMI270_ID])[:-1]
bmi270_config = full_config.get_config_for_path(bmi270_path)
if bmi270_config.get(CONF_AUX_DEVICE) != "BMM150":
raise cv.Invalid(
"This sensor requires the parent bmi270's 'aux_device' to be set to 'BMM150'",
+1 -2
View File
@@ -80,8 +80,7 @@ void BP1658CJ::loop() {
this->update_ = false;
}
void BP1658CJ::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(state * 0x3FF);
void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) {
if (this->pwm_amounts_[channel] != value) {
this->update_ = true;
this->update_channel_ = channel;
+18 -1
View File
@@ -2,12 +2,15 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>
namespace esphome::bp1658cj {
class BP1658CJ final : public Component {
public:
class Channel;
void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
void set_max_power_color_channels(uint8_t max_power_color_channels) {
@@ -26,9 +29,23 @@ class BP1658CJ final : public Component {
/// Send new values if they were updated.
void loop() override;
void write_channel(uint8_t channel, float state);
class Channel final : public output::FloatOutput {
public:
void set_parent(BP1658CJ *parent) { parent_ = parent; }
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
void write_state(float state) override {
auto amount = static_cast<uint16_t>(state * 0x3FF);
this->parent_->set_channel_value_(this->channel_, amount);
}
BP1658CJ *parent_;
uint8_t channel_;
};
protected:
void set_channel_value_(uint8_t channel, uint16_t value);
void write_bit_(bool value);
void write_byte_(uint8_t data);
void write_buffer_(uint8_t *buffer, uint8_t size);
+9 -5
View File
@@ -1,3 +1,4 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
@@ -7,15 +8,16 @@ from . import BP1658CJ
DEPENDENCIES = ["bp1658cj"]
Channel = BP1658CJ.class_("Channel", output.FloatOutput)
CONF_BP1658CJ_ID = "bp1658cj_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ),
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
}
)
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
@@ -24,6 +26,8 @@ FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
async def to_code(config: ConfigType) -> None:
await output.sub_float_outputs(config, parent=config[CONF_BP1658CJ_ID]).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
var = cg.new_Pvariable(config[CONF_ID])
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_BP1658CJ_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
+2 -2
View File
@@ -7,7 +7,6 @@ from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, CONF_WATCHDOG
from esphome.core import CORE, ID, EsphomeError, Lambda, TimePeriod
import esphome.final_validate as fv
from esphome.types import ConfigType, TemplateArgsType
CODEOWNERS = ["@Rapsssito"]
@@ -360,7 +359,8 @@ async def bq25186_configure_to_code(
parent = await cg.get_variable(config[CONF_ID])
# Options the action leaves out fall back to the component's configuration
parent_config = fv.get_declaring_config(config[CONF_ID], CORE.config)
path = CORE.config.get_path_for_id(config[CONF_ID])[:-1]
parent_config = CORE.config.get_config_for_path(path)
merged = {**parent_config, **config}
try:
_check_watchdog(merged)
+7 -4
View File
@@ -28,10 +28,13 @@ bool BQ25186Component::write_register_(uint8_t reg, uint8_t value) {
}
bool BQ25186Component::update_register_(uint8_t reg, uint8_t mask, uint8_t value) {
if (this->update_register(reg, mask, value))
return true;
ESP_LOGW(TAG, "Failed to update register 0x%02X", reg);
return false;
uint8_t reg_value;
if (!this->read_byte(reg, &reg_value)) {
ESP_LOGW(TAG, "Failed to read register 0x%02X", reg);
return false;
}
reg_value = (reg_value & ~mask) | (value & mask);
return this->write_register_(reg, reg_value);
}
uint8_t BQ25186Component::expected_register_(size_t index) const {
+1 -1
View File
@@ -71,12 +71,12 @@ class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
protected:
bool write_register_(uint8_t reg, uint8_t value);
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
uint8_t expected_register_(size_t index) const;
bool config_matches_(const BQ25186StatusRegisters &regs) const;
#ifdef USE_BQ25186_PG_GPO
void check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending);
#endif
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
#ifdef BQ25186_BINARY_SENSOR_COUNT
StaticVector<BQ25186BinarySensor *, BQ25186_BINARY_SENSOR_COUNT> binary_sensors_;
@@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BUTTON
#include "bq25186.h"
#include "esphome/components/button/button.h"
namespace esphome::bq25186 {
/// Writes one SHIP_RST field when pressed
class BQ25186Button final : public button::Button, public Parented<BQ25186Component> {
public:
BQ25186Button(uint8_t mask, uint8_t value) : mask_(mask), value_(value) {}
protected:
void press_action() override { this->parent_->write_ship_rst(this->mask_, this->value_); }
const uint8_t mask_;
const uint8_t value_;
};
} // namespace esphome::bq25186
#endif // USE_BUTTON
@@ -14,7 +14,11 @@ void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
if (value == this->last_)
return;
this->last_ = value;
this->publish_state(StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX));
const ProgmemStr name = StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX);
char text[32];
ESPHOME_strncpy_P(text, reinterpret_cast<ESPHOME_PGM_P>(name), sizeof(text));
text[sizeof(text) - 1] = '\0';
this->publish_state(text);
}
} // namespace esphome::bq25186
+7 -4
View File
@@ -1,3 +1,4 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
@@ -9,10 +10,11 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
DEPENDENCIES = ["bq25186"]
BQ25186Button = bq25186_ns.class_("BQ25186Button", button.Button)
# SHIP_RST: bit 7 is the software reset, bits 6:5 the shutdown, ship and hardware reset codes
SOFTWARE_RESET_MASK = 0x80
@@ -31,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): button.button_schema(
button.ApplyButton,
BQ25186Button,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=icon,
device_class=device_class,
@@ -44,6 +46,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
buttons = button.sub_buttons(config, parent=config[CONF_BQ25186_ID])
for key, mask, value, _, _ in _BUTTONS:
await buttons(key, None, apply=f"write_ship_rst({mask:#04x}, {value:#04x})")
if (conf := config.get(key)) is not None:
var = await button.new_button(conf, mask, value)
await cg.register_parented(var, config[CONF_BQ25186_ID])
+11 -7
View File
@@ -1,6 +1,7 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -9,6 +10,7 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_PRESS,
CONF_WEB_SERVER,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_IDENTIFY,
DEVICE_CLASS_RESTART,
@@ -19,12 +21,11 @@ from esphome.const import (
ICON_RESTART,
ICON_RESTART_ALERT,
)
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 (
ApplyEntity,
SubEntities,
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -52,13 +53,11 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
_BUTTON_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTButtonComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): validate_device_class,
@@ -136,7 +135,12 @@ async def setup_button_core_(var: MockObj, config: ConfigType) -> None:
setup_device_class(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 web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
async def register_button(var: MockObj, config: ConfigType) -> None:
+4 -2
View File
@@ -42,8 +42,10 @@ void CAP1188Component::finish_setup_() {
}
// Set sensitivity
// The threshold replaces the upper nibble and is ORed into the lower one, as before
this->update_register(CAP1188_SENSITVITY, 0xF0 | this->touch_threshold_, this->touch_threshold_);
uint8_t sensitivity = 0;
this->read_byte(CAP1188_SENSITVITY, &sensitivity);
sensitivity = sensitivity & 0x0f;
this->write_byte(CAP1188_SENSITVITY, sensitivity | this->touch_threshold_);
// Allow multiple touches
this->write_byte(CAP1188_MULTI_TOUCH, this->allow_multiple_touches_);
+4 -7
View File
@@ -2,7 +2,6 @@
#include "cc1101pa.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cmath>
#include <cstring>
@@ -15,9 +14,6 @@ namespace esphome::cc1101 {
ESPHOME_LOG_TAG(TAG, "cc1101");
static constexpr size_t TX_FIFO_SIZE = 64;
// MDMCFG2 MOD_FORMAT values
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) {
int e_tmp;
float m_tmp = std::frexp(value, &e_tmp);
@@ -242,6 +238,8 @@ void CC1101Component::loop() {
}
void CC1101Component::dump_config() {
static const char *const MODULATION_NAMES[] = {"2-FSK", "GFSK", "UNUSED", "ASK/OOK",
"4-FSK", "UNUSED", "UNUSED", "MSK"};
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
XTAL_FREQUENCY / (1 << 16);
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
@@ -255,9 +253,8 @@ void CC1101Component::dump_config() {
" Symbol Rate: %.0f baud\n"
" Filter Bandwidth: %.1f Hz\n"
" Output Power: %.1f dBm",
this->chip_id_, freq, this->state_.CHANNR,
LOG_STR_ARG(ModulationStrings::get_log_str(this->state_.MOD_FORMAT & 0x07, 0)), symbol_rate, bw,
this->output_power_effective_);
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
symbol_rate, bw, this->output_power_effective_);
LOG_PIN(" CS Pin: ", this->cs_);
}
+12 -8
View File
@@ -3,6 +3,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION_STATE_TOPIC,
@@ -48,15 +49,15 @@ from esphome.const import (
CONF_TEMPERATURE_STEP,
CONF_UNIT_OF_MEASUREMENT,
CONF_VISUAL,
CONF_WEB_SERVER,
UNIT_KELVIN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
TemperatureUnitResolver,
convert_visual_temperatures,
declare_fixed_temperature_unit,
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
setup_entity_temperature_unit,
@@ -176,13 +177,11 @@ _visual_temperatures_in_entity_unit = partial(
_CLIMATE_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTClimateComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
{
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
@@ -354,8 +353,10 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
cg.add(var.set_visual_max_humidity_override(max_humidity))
setup_entity_temperature_unit(var, config)
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
if mqtt_ is not None:
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if (action_state_topic := config.get(CONF_ACTION_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_action_state_topic(action_state_topic))
if (away_command_topic := config.get(CONF_AWAY_COMMAND_TOPIC)) is not None:
@@ -485,6 +486,9 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
def set_custom_fan_modes(var: MockObj, modes: list[str]) -> None:
"""Point the climate at a shared flash table of custom fan modes."""
+16 -5
View File
@@ -13,10 +13,19 @@ from esphome.const import (
from esphome.core.entity_helpers import inherit_property_from
from esphome.types import ConfigType
CONFIG_SCHEMA = sensor.sensor_schema(sensor.Sensor).extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(sensor.Sensor),
}
from .. import copy_ns
CopySensor = copy_ns.class_("CopySensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = (
sensor.sensor_schema(CopySensor)
.extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(sensor.Sensor),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All(
@@ -31,4 +40,6 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
sensor.follow_sensor(var, await cg.get_variable(config[CONF_SOURCE_ID]))
await cg.register_component(var, config)
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
@@ -0,0 +1,16 @@
#include "copy_sensor.h"
#include "esphome/core/log.h"
namespace esphome::copy {
ESPHOME_LOG_TAG(TAG, "copy.sensor");
void CopySensor::setup() {
source_->add_on_state_callback([this](float value) { this->publish_state(value); });
if (source_->has_state())
this->publish_state(source_->state);
}
void CopySensor::dump_config() { LOG_SENSOR("", "Copy Sensor", this); }
} // namespace esphome::copy
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::copy {
class CopySensor final : public sensor::Sensor, public Component {
public:
void set_source(sensor::Sensor *source) { source_ = source; }
void setup() override;
void dump_config() override;
protected:
sensor::Sensor *source_;
};
} // namespace esphome::copy
@@ -5,10 +5,19 @@ from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
from esphome.core.entity_helpers import inherit_property_from
from esphome.types import ConfigType
CONFIG_SCHEMA = text_sensor.text_sensor_schema(text_sensor.TextSensor).extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(text_sensor.TextSensor),
}
from .. import copy_ns
CopyTextSensor = copy_ns.class_("CopyTextSensor", text_sensor.TextSensor, cg.Component)
CONFIG_SCHEMA = (
text_sensor.text_sensor_schema(CopyTextSensor)
.extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(text_sensor.TextSensor),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All(
@@ -19,4 +28,6 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = await text_sensor.new_text_sensor(config)
text_sensor.follow_text_sensor(var, await cg.get_variable(config[CONF_SOURCE_ID]))
await cg.register_component(var, config)
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
@@ -0,0 +1,16 @@
#include "copy_text_sensor.h"
#include "esphome/core/log.h"
namespace esphome::copy {
ESPHOME_LOG_TAG(TAG, "copy.text_sensor");
void CopyTextSensor::setup() {
source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
if (source_->has_state())
this->publish_state(source_->state);
}
void CopyTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Copy Sensor", this); }
} // namespace esphome::copy
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::copy {
class CopyTextSensor final : public text_sensor::TextSensor, public Component {
public:
void set_source(text_sensor::TextSensor *source) { source_ = source; }
void setup() override;
void dump_config() override;
protected:
text_sensor::TextSensor *source_;
};
} // namespace esphome::copy
+12 -8
View File
@@ -3,6 +3,7 @@ import logging
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -22,6 +23,7 @@ from esphome.const import (
CONF_TILT_COMMAND_TOPIC,
CONF_TILT_STATE_TOPIC,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
DEVICE_CLASS_AWNING,
DEVICE_CLASS_BLIND,
DEVICE_CLASS_CURTAIN,
@@ -34,10 +36,9 @@ from esphome.const import (
DEVICE_CLASS_SHUTTER,
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_device_class,
setup_entity,
@@ -111,13 +112,11 @@ TRIGGERS = {
_COVER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTCoverComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
cv.requires_component("mqtt"), cv.boolean
),
@@ -203,8 +202,10 @@ async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
if mqtt_ is not None:
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if (position_state_topic := config.get(CONF_POSITION_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_position_state_topic(position_state_topic))
if (
@@ -219,6 +220,9 @@ async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
cg.add(mqtt_.set_custom_tilt_command_topic(tilt_command_topic))
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
async def register_cover(var: MockObj, config: ConfigType) -> None:
if not CORE.has_id(config[CONF_ID]):
+22 -15
View File
@@ -28,23 +28,30 @@ void CSE7766Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
this->read_available<CSE7766_RAW_DATA_SIZE>(
[this](uint8_t byte) {
this->raw_data_[this->raw_data_index_] = byte;
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
return;
}
uint8_t buf[CSE7766_RAW_DATA_SIZE];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
this->parse_data_();
this->status_clear_warning();
}
for (size_t i = 0; i < to_read; i++) {
this->raw_data_[this->raw_data_index_] = buf[i];
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
continue;
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
},
avail);
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
this->parse_data_();
this->status_clear_warning();
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
}
}
}
bool CSE7766Component::check_byte_() {
@@ -6,7 +6,12 @@ ESPHOME_LOG_TAG(TAG, "cst226.touchscreen");
void CST226Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
this->set_timeout(30, [this] { this->continue_setup_(); });
} else {
this->continue_setup_();
@@ -43,7 +43,9 @@ void CST328Touchscreen::setup() {
}
void CST328Touchscreen::reset_device_() {
pulse_reset(this->reset_pin_, {.low_ms = 5});
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
this->set_timeout(CST328_TRANSITION_TIMEOUT, [this] { this->continue_setup_(); });
}
@@ -52,7 +52,12 @@ void CST816Touchscreen::continue_setup_() {
void CST816Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
this->set_timeout(30, [this] { this->continue_setup_(); });
} else {
this->continue_setup_();
@@ -10,7 +10,14 @@ static constexpr uint32_t REREAD_TIMEOUT_ID = 1;
static constexpr uint32_t REREAD_DELAY_MS = 20;
void CST9220Touchscreen::setup() {
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 10});
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
// Wait for the controller to leave its bootloader before talking to it.
this->set_timeout(30, [this] { this->continue_setup_(); });
}
+12 -7
View File
@@ -163,21 +163,26 @@ void CW2017::update() {
if (!this->ready_)
return;
bool ok = true;
if (this->battery_level_sensor_ != nullptr) {
uint8_t soc = 0;
ok = this->read_byte(REG_SOC, &soc) && soc <= 100;
if (ok)
if (this->read_byte(REG_SOC, &soc) && soc <= 100) {
this->battery_level_sensor_->publish_state(soc);
} else {
this->status_set_warning();
return;
}
}
if (ok && this->battery_voltage_sensor_ != nullptr) {
if (this->battery_voltage_sensor_ != nullptr) {
uint16_t raw = 0;
ok = this->read_byte_16(REG_VCELL, &raw);
if (ok)
if (this->read_byte_16(REG_VCELL, &raw)) {
this->battery_voltage_sensor_->publish_state((raw & VCELL_MASK) * VCELL_VOLTS_PER_LSB);
} else {
this->status_set_warning();
return;
}
}
this->status_comm_result(ok);
this->status_clear_warning();
}
void CW2017::dump_config() {
+4 -2
View File
@@ -12,8 +12,8 @@ class CW2017 : public PollingComponent, public i2c::I2CDevice {
public:
explicit CW2017(const uint8_t *profile) : profile_(profile) {}
SUB_SENSOR(battery_level)
SUB_SENSOR(battery_voltage)
void set_battery_level_sensor(sensor::Sensor *sensor) { this->battery_level_sensor_ = sensor; }
void set_battery_voltage_sensor(sensor::Sensor *sensor) { this->battery_voltage_sensor_ = sensor; }
void setup() override;
void update() override;
@@ -29,6 +29,8 @@ class CW2017 : public PollingComponent, public i2c::I2CDevice {
void wait_ready_(uint8_t attempt);
const uint8_t *profile_;
sensor::Sensor *battery_level_sensor_{nullptr};
sensor::Sensor *battery_voltage_sensor_{nullptr};
bool ready_{false};
bool restart_pending_{false};
uint8_t init_attempts_{0};
+13 -6
View File
@@ -48,14 +48,15 @@ void DAC7678Output::dump_config() {
}
}
void DAC7678Output::register_channel(uint8_t channel) {
this->min_channel_ = std::min(this->min_channel_, channel);
this->max_channel_ = std::max(this->max_channel_, channel);
ESP_LOGV(TAG, "Registered channel: %01u", channel);
void DAC7678Output::register_channel(DAC7678Channel *channel) {
auto c = channel->channel_;
this->min_channel_ = std::min(this->min_channel_, c);
this->max_channel_ = std::max(this->max_channel_, c);
channel->set_parent(this);
ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_);
}
void DAC7678Output::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(roundf(state * 0xFFF));
void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
if (channel >= std::size(this->dac_input_reg_))
return;
if (this->dac_input_reg_[channel] != value) {
@@ -70,4 +71,10 @@ void DAC7678Output::write_channel(uint8_t channel, float state) {
this->status_clear_warning();
}
void DAC7678Channel::write_state(float state) {
const float input_rounded = roundf(state * this->full_scale_);
auto input = static_cast<uint16_t>(input_rounded);
this->parent_->set_channel_value_(this->channel_, input);
}
} // namespace esphome::dac7678
+23 -2
View File
@@ -1,17 +1,34 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::dac7678 {
class DAC7678Output;
class DAC7678Channel final : public output::FloatOutput, public Parented<DAC7678Output> {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
protected:
friend class DAC7678Output;
const uint16_t full_scale_ = 0xFFF;
void write_state(float state) override;
uint8_t channel_;
};
/// DAC7678 float output component.
class DAC7678Output final : public Component, public i2c::I2CDevice {
public:
DAC7678Output() {}
void register_channel(uint8_t channel);
void write_channel(uint8_t channel, float state);
void register_channel(DAC7678Channel *channel);
void set_internal_reference(const bool value) { this->internal_reference_ = value; }
@@ -20,8 +37,12 @@ class DAC7678Output final : public Component, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
friend DAC7678Channel;
bool internal_reference_;
void set_channel_value_(uint8_t channel, uint16_t value);
uint8_t min_channel_{0xFF};
uint8_t max_channel_{0x00};
uint16_t dac_input_reg_[8] = {
+11 -8
View File
@@ -2,17 +2,19 @@ import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
from . import DAC7678Output
from . import DAC7678Output, dac7678_ns
DEPENDENCIES = ["dac7678"]
DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput)
CONF_DAC7678_ID = "dac7678_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(output.ApplyFloatOutput),
cv.Required(CONF_ID): cv.declare_id(DAC7678Channel),
cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
}
@@ -24,9 +26,10 @@ FINAL_VALIDATE_SCHEMA = output.final_validate_unique_channel(
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DAC7678_ID])
await output.sub_float_outputs(config, parent=hub).create(
config, None, config[CONF_CHANNEL], apply="write_channel(index, state)"
)
cg.add(hub.register_channel(config[CONF_CHANNEL]))
async def to_code(config: ConfigType) -> MockObj:
paren = await cg.get_variable(config[CONF_DAC7678_ID])
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(paren.register_channel(var))
await output.register_output(var, config)
return var
+14 -17
View File
@@ -2,6 +2,7 @@ from collections.abc import Callable
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, time, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_DATE,
@@ -19,12 +20,12 @@ from esphome.const import (
CONF_TIME_ID,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_WEB_SERVER,
CONF_YEAR,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
mqtt_component_class,
queue_entity_register,
setup_entity,
)
@@ -37,10 +38,6 @@ DOMAIN = "datetime"
IS_PLATFORM_COMPONENT = True
datetime_ns = cg.esphome_ns.namespace("datetime")
# Matches time.RealTimeClock by name without importing the time component
_RealTimeClock = cg.esphome_ns.namespace("time").class_(
"RealTimeClock", cg.PollingComponent
)
DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase)
DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
@@ -67,7 +64,7 @@ DATETIME_MODES = [
def _validate_time_present(config: ConfigType) -> ConfigType:
config = config.copy()
if CONF_ON_TIME in config and CONF_TIME_ID not in config:
time_id = cv.use_id(_RealTimeClock)(None)
time_id = cv.use_id(time.RealTimeClock)(None)
config[CONF_TIME_ID] = time_id
return config
@@ -76,10 +73,10 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.Schema(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
cv.Optional(CONF_TIME_ID): cv.use_id(_RealTimeClock),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
).add_extra(_validate_time_present)
@@ -90,9 +87,7 @@ def date_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTDateComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent),
cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True),
}
)
@@ -103,9 +98,7 @@ def time_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTTimeComponent")
),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent),
cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True),
cv.Optional(CONF_ON_TIME): automation.validate_automation(
{
@@ -122,7 +115,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
{
cv.GenerateID(): cv.declare_id(class_),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTDateTimeComponent")
mqtt.MQTTDateTimeComponent
),
cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of(
"DATETIME", upper=True
@@ -139,7 +132,11 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
@setup_entity("datetime")
async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None:
await entity_helpers.setup_entity_integrations(var, config)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
# The state callback carries nothing; the automation gets the value as an ESPTime.
for conf in config.get(CONF_ON_VALUE, []):
await automation.build_callback_automation(
+146 -133
View File
@@ -135,140 +135,153 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
}
void DFPlayer::loop() {
this->read_available([this](uint8_t byte) {
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
return;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
return;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
return;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
return;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
default:
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
return;
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
});
avail -= to_read;
for (size_t bi = 0; bi < to_read; bi++) {
uint8_t byte = buf[bi];
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
continue;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG,
"Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
default:
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
continue;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
}
}
}
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
@@ -42,7 +42,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_LINE_READER")
await uart.new_uart_component(config)
+16 -18
View File
@@ -1,7 +1,5 @@
#pragma once
#include <algorithm>
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
@@ -29,12 +27,9 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
TEMPLATABLE_VALUE(float, det_max4)
void play(const Ts &...x) override {
// Worst case: stop, factory reset, six settings, save, start
if (!this->parent_->has_queue_room(10))
return;
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, false);
this->parent_->enqueue(make_unique<PowerCommand>(0));
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_FACTORY_RESET);
this->parent_->enqueue(make_unique<FactoryResetCommand>());
}
if (this->det_min1_.has_value() && this->det_max1_.has_value()) {
// Each segment is read once, so the guard and the command see the same values.
@@ -42,10 +37,10 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
if (min1 >= 0) {
const float max1 = this->det_max1_.value(x...);
if (max1 >= 0) {
this->parent_->enqueue_det_range_cfg(min1, max1, this->det_min2_.value_or(x..., -1),
this->det_max2_.value_or(x..., -1), this->det_min3_.value_or(x..., -1),
this->det_max3_.value_or(x..., -1), this->det_min4_.value_or(x..., -1),
this->det_max4_.value_or(x..., -1));
this->parent_->enqueue(make_unique<DetRangeCfgCommand>(
min1, max1, this->det_min2_.value_or(x..., -1), this->det_max2_.value_or(x..., -1),
this->det_min3_.value_or(x..., -1), this->det_max3_.value_or(x..., -1),
this->det_min4_.value_or(x..., -1), this->det_max4_.value_or(x..., -1)));
}
}
}
@@ -53,35 +48,38 @@ class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented
float detect = this->delay_after_detect_.value(x...);
float disappear = this->delay_after_disappear_.value(x...);
if (detect >= 0 && disappear >= 0) {
this->parent_->enqueue_set_latency(detect, disappear);
this->parent_->enqueue(make_unique<SetLatencyCommand>(detect, disappear));
}
}
if (this->start_after_power_on_.has_value()) {
int8_t val = this->start_after_power_on_.value(x...);
if (val >= 0) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSOR_CFG_START, val);
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(val));
}
}
if (this->turn_on_led_.has_value()) {
int8_t val = this->turn_on_led_.value(x...);
if (val >= 0) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_LED_MODE, val);
this->parent_->enqueue(make_unique<LedModeCommand>(val));
}
}
if (this->presence_via_uart_.has_value()) {
int8_t val = this->presence_via_uart_.value(x...);
if (val >= 0) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_UART_OUTPUT, val);
this->parent_->enqueue(make_unique<UartOutputCommand>(val));
}
}
if (this->sensitivity_.has_value()) {
int8_t val = this->sensitivity_.value(x...);
if (val >= 0) {
this->parent_->enqueue(CommandType::COMMAND_TYPE_SENSITIVITY, std::min<int8_t>(val, 9));
if (val > 9) {
val = 9;
}
this->parent_->enqueue(make_unique<SensitivityCommand>(val));
}
}
this->parent_->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
this->parent_->enqueue(CommandType::COMMAND_TYPE_POWER, true);
this->parent_->enqueue(make_unique<SaveCfgCommand>());
this->parent_->enqueue(make_unique<PowerCommand>(1));
}
};
@@ -0,0 +1,332 @@
#include "commands.h"
#include <cmath>
#include "esphome/core/log.h"
#include "dfrobot_sen0395.h"
namespace esphome::dfrobot_sen0395 {
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395.commands");
uint8_t Command::execute(DfrobotSen0395Component *parent) {
this->parent_ = parent;
if (this->cmd_sent_) {
if (this->parent_->read_message_()) {
std::string message(this->parent_->read_buffer_);
if (message.rfind("is not recognized as a CLI command") != std::string::npos) {
ESP_LOGD(TAG, "Command not recognized properly by sensor");
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying");
return 0;
} else {
this->parent_->find_prompt_();
return 1; // Command done
}
}
uint8_t rc = on_message(message);
if (rc == 2) {
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying");
return 0;
} else {
this->parent_->find_prompt_();
return 1; // Command done
}
} else if (rc == 0) {
return 0;
} else {
this->parent_->find_prompt_();
return 1;
}
}
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
ESP_LOGD(TAG, "Command timeout");
if (this->retries_left_ > 0) {
this->retries_left_ -= 1;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying");
} else {
return 1; // Command done
}
}
} else if (this->parent_->send_cmd_(this->cmd_.c_str(), this->cmd_duration_ms_)) {
this->cmd_sent_ = true;
}
return 0; // Command not done yet
}
uint8_t ReadStateCommand::execute(DfrobotSen0395Component *parent) {
this->parent_ = parent;
if (this->parent_->read_message_()) {
std::string message(this->parent_->read_buffer_);
if (message.rfind("$JYBSS,0, , , *") != std::string::npos) {
this->parent_->set_detected_(false);
this->parent_->set_active(true);
return 1; // Command done
} else if (message.rfind("$JYBSS,1, , , *") != std::string::npos) {
this->parent_->set_detected_(true);
this->parent_->set_active(true);
return 1; // Command done
}
}
if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) {
return 1; // Command done, timeout
}
return 0; // Command not done yet.
}
uint8_t ReadStateCommand::on_message(std::string &message) { return 1; }
uint8_t PowerCommand::on_message(std::string &message) {
if (message == "sensor stopped already") {
this->parent_->set_active(false);
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
return 1; // Command done
} else if (message == "sensor started already") {
this->parent_->set_active(true);
ESP_LOGI(TAG, "Started sensor (already started)");
return 1; // Command done
} else if (message == "new parameter isn't save, can't startSensor") {
this->parent_->set_active(false);
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
return 1; // Command done
} else if (message == "Done") {
this->parent_->set_active(this->power_on_);
if (this->power_on_) {
ESP_LOGI(TAG, "Started sensor");
} else {
ESP_LOGI(TAG, "Stopped sensor");
}
return 1; // Command done
}
return 0; // Command not done yet.
}
DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3,
float min4, float max4) {
// TODO: Print warning when values are rounded
if (min1 < 0 || max1 < 0) {
this->min1_ = min1 = 0;
this->max1_ = max1 = 0;
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
ESP_LOGW(TAG, "DetRangeCfgCommand invalid input parameters. Using range config 0 0.");
this->cmd_ = "detRangeCfg -1 0 0";
} else if (min2 < 0 || max2 < 0) {
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15f, max1 / 0.15f);
this->cmd_ = buf;
} else if (min3 < 0 || max3 < 0) {
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
max2 / 0.15f);
this->cmd_ = buf;
} else if (min4 < 0 || max4 < 0) {
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
this->min4_ = min4 = this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f,
max2 / 0.15f, min3 / 0.15f, max3 / 0.15f);
this->cmd_ = buf;
} else {
this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f;
this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f;
this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f;
this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f;
this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f;
this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f;
this->min4_ = min4 = roundf(min4 / 0.15f) * 0.15f;
this->max4_ = max4 = roundf(max4 / 0.15f) * 0.15f;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f,
min2 / 0.15f, max2 / 0.15f, min3 / 0.15f, max3 / 0.15f, min4 / 0.15f, max4 / 0.15f);
this->cmd_ = buf;
}
this->min1_ = min1;
this->max1_ = max1;
this->min2_ = min2;
this->max2_ = max2;
this->min3_ = min3;
this->max3_ = max3;
this->min4_ = min4;
this->max4_ = max4;
};
uint8_t DetRangeCfgCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure range config. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG,
"Updated detection area config:\n"
" Detection area 1 from %.02fm to %.02fm.",
this->min1_, this->max1_);
if (this->min2_ >= 0 && this->max2_ >= 0) {
ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_);
}
if (this->min3_ >= 0 && this->max3_ >= 0) {
ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_);
}
if (this->min4_ >= 0 && this->max4_ >= 0) {
ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_);
}
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet.
}
SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) {
delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f;
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
// max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32
char buf[32];
snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
this->cmd_ = buf;
};
uint8_t SetLatencyCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG,
"Updated output latency config:\n"
" Signal that someone was detected is delayed by %.03f s.\n"
" Signal that nobody is detected anymore is delayed by %.03f s.",
this->delay_after_detection_, this->delay_after_disappear_);
ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SensorCfgStartCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure sensor startup behavior. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Updated sensor startup behavior:");
if (startup_mode_) {
this->parent_->set_start_after_boot(true);
ESP_LOGI(TAG, "Sensor will start automatically after power-on.");
} else {
this->parent_->set_start_after_boot(false);
ESP_LOGI(TAG, "Sensor needs to be started manually after power-on.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t FactoryResetCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot factory reset. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Sensor factory reset done.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t ResetSystemCommand::on_message(std::string &message) {
if (message == "leapMMW:/>") {
ESP_LOGI(TAG, "Restarted sensor.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SaveCfgCommand::on_message(std::string &message) {
if (message == "no parameter has changed") {
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t LedModeCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set led mode. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set led mode done.");
if (this->active_) {
this->parent_->set_led_active(true);
ESP_LOGI(TAG, "Sensor LED will blink.");
} else {
this->parent_->set_led_active(false);
ESP_LOGI(TAG, "Turned off LED.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t UartOutputCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set uart output mode. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set uart mode done.");
if (this->active_) {
this->parent_->set_uart_presence_active(true);
ESP_LOGI(TAG, "Presence information is sent via UART and GPIO.");
} else {
this->parent_->set_uart_presence_active(false);
ESP_LOGI(TAG, "Presence information is only sent via GPIO.");
}
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
uint8_t SensitivityCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot set sensitivity. Sensor is not stopped!");
return 1; // Command done
} else if (message == "Done") {
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", this->sensitivity_);
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done
}
return 0; // Command not done yet
}
} // namespace esphome::dfrobot_sen0395
@@ -0,0 +1,152 @@
#pragma once
#include <cstdint>
#include <string>
#include "esphome/core/helpers.h"
namespace esphome::dfrobot_sen0395 {
class DfrobotSen0395Component;
// Use command queue and time stamps to avoid blocking.
// When component has run time, check if minimum time (1s) between
// commands has passed. After that run a command from the queue.
class Command {
public:
virtual ~Command() = default;
virtual uint8_t execute(DfrobotSen0395Component *parent);
virtual uint8_t on_message(std::string &message) = 0;
protected:
DfrobotSen0395Component *parent_{nullptr};
std::string cmd_;
bool cmd_sent_{false};
int8_t retries_left_{2};
uint32_t cmd_duration_ms_{1000};
uint32_t timeout_ms_{1500};
};
class ReadStateCommand : public Command {
public:
ReadStateCommand() { timeout_ms_ = 500; }
uint8_t execute(DfrobotSen0395Component *parent) override;
uint8_t on_message(std::string &message) override;
};
class PowerCommand : public Command {
public:
PowerCommand(bool power_on) : power_on_(power_on) {
if (power_on) {
cmd_ = "sensorStart";
} else {
cmd_ = "sensorStop";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool power_on_;
};
class DetRangeCfgCommand : public Command {
public:
DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, float min4, float max4);
uint8_t on_message(std::string &message) override;
protected:
float min1_, max1_, min2_, max2_, min3_, max3_, min4_, max4_;
// TODO: Set min max values in component, so they can be published as sensor.
};
class SetLatencyCommand : public Command {
public:
SetLatencyCommand(float delay_after_detection, float delay_after_disappear);
uint8_t on_message(std::string &message) override;
protected:
float delay_after_detection_;
float delay_after_disappear_;
};
class SensorCfgStartCommand : public Command {
public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd);
}
uint8_t on_message(std::string &message) override;
protected:
bool startup_mode_;
};
class FactoryResetCommand : public Command {
public:
FactoryResetCommand() { cmd_ = "factoryReset 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; };
uint8_t on_message(std::string &message) override;
};
class ResetSystemCommand : public Command {
public:
ResetSystemCommand() { cmd_ = "resetSystem"; }
uint8_t on_message(std::string &message) override;
};
class SaveCfgCommand : public Command {
public:
SaveCfgCommand() {
cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89";
cmd_duration_ms_ = 3000;
timeout_ms_ = 3500;
}
uint8_t on_message(std::string &message) override;
};
class LedModeCommand : public Command {
public:
LedModeCommand(bool active) : active_(active) {
if (active) {
cmd_ = "setLedMode 1 0";
} else {
cmd_ = "setLedMode 1 1";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool active_;
};
class UartOutputCommand : public Command {
public:
UartOutputCommand(bool active) : active_(active) {
if (active) {
cmd_ = "setUartOutput 1 1";
} else {
cmd_ = "setUartOutput 1 0";
}
};
uint8_t on_message(std::string &message) override;
protected:
bool active_;
};
class SensitivityCommand : public Command {
public:
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9)
sensitivity_ = sensitivity = 9;
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd);
};
uint8_t on_message(std::string &message) override;
protected:
uint8_t sensitivity_;
};
} // namespace esphome::dfrobot_sen0395
@@ -1,27 +1,13 @@
#include "dfrobot_sen0395.h"
#include <cmath>
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::dfrobot_sen0395 {
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395");
static constexpr float DET_RANGE_STEP_M = 0.15f;
static constexpr float LATENCY_STEP_S = 0.025f;
// Sent after a reset with no line end
static constexpr char PROMPT[] PROGMEM = "leapMMW:/>";
// What each CommandType cannot do while the sensor runs; empty for commands that do not need it stopped
PROGMEM_STRING_TABLE(BusyActions, "", "configure range config", "configure output latency",
"configure sensor startup behavior", "factory reset", "", "", "set led mode",
"set uart output mode", "set sensitivity");
static_assert(BusyActions::COUNT == static_cast<size_t>(CommandType::COMMAND_TYPE_SENSITIVITY) + 1,
"BusyActions needs one entry per CommandType");
const char ASCII_CR = 0x0D;
const char ASCII_LF = 0x0A;
void DfrobotSen0395Component::dump_config() {
ESP_LOGCONFIG(TAG, "Dfrobot Mmwave Radar:");
@@ -37,287 +23,72 @@ void DfrobotSen0395Component::dump_config() {
}
void DfrobotSen0395Component::loop() {
if (this->cmd_queue_.empty()) {
this->read_state_();
return;
if (cmd_queue_.is_empty()) {
// Command queue empty. Read sensor state.
cmd_queue_.enqueue(make_unique<ReadStateCommand>());
}
// Commands are non-blocking and need to be called repeatedly
if (this->run_command_(this->cmd_queue_.front(), App.get_loop_component_start_time())) {
this->cmd_queue_.pop();
this->cmd_sent_ = false;
this->retries_left_ = 2;
// Commands are non-blocking and need to be called repeatedly.
if (cmd_queue_.process(this)) {
// Dequeue if command is done
cmd_queue_.dequeue();
}
}
bool DfrobotSen0395Component::enqueue(CommandType type, uint8_t arg) {
if (!this->cmd_queue_.push({type, arg})) {
ESP_LOGE(TAG, "Command queue is full");
return false;
}
return true;
int8_t DfrobotSen0395Component::enqueue(std::unique_ptr<Command> cmd) {
return cmd_queue_.enqueue(std::move(cmd)); // Transfer ownership using std::move
}
bool DfrobotSen0395Component::has_queue_room(size_t count) const {
if (COMMAND_QUEUE_SIZE - this->cmd_queue_.size() >= count)
return true;
ESP_LOGE(TAG, "Command queue is full");
return false;
}
void DfrobotSen0395Component::enqueue_det_range_cfg(float min1, float max1, float min2, float max2, float min3,
float max3, float min4, float max4) {
// TODO: Print warning when values are rounded
const float values[8] = {min1, max1, min2, max2, min3, max3, min4, max4};
int16_t steps[8];
bool in_use = true;
for (size_t i = 0; i < 8; i += 2) {
in_use = in_use && values[i] >= 0 && values[i + 1] >= 0;
for (size_t j = i; j < i + 2; j++) {
steps[j] = in_use ? static_cast<int16_t>(clamp(roundf(values[j] / DET_RANGE_STEP_M), 0.0f, 32767.0f)) : -1;
}
}
if (steps[0] < 0) {
ESP_LOGW(TAG, "Invalid detection range, using 0 0");
steps[0] = steps[1] = 0;
}
if (this->enqueue(CommandType::COMMAND_TYPE_DET_RANGE_CFG))
memcpy(this->det_range_steps_, steps, sizeof(steps));
}
void DfrobotSen0395Component::enqueue_set_latency(float delay_after_detection, float delay_after_disappear) {
if (!this->enqueue(CommandType::COMMAND_TYPE_SET_LATENCY))
return;
// 1638.375 s is 65535 steps
this->latency_steps_[0] =
static_cast<uint16_t>(clamp(std::round(delay_after_detection / LATENCY_STEP_S), 0.0f, 65535.0f));
this->latency_steps_[1] =
static_cast<uint16_t>(clamp(std::round(delay_after_disappear / LATENCY_STEP_S), 0.0f, 65535.0f));
}
void DfrobotSen0395Component::enqueue_saved_setting(CommandType type, bool value) {
const bool was_active = this->active_;
if (!this->has_queue_room(was_active ? 4 : 2))
return;
if (was_active)
this->enqueue(CommandType::COMMAND_TYPE_POWER, false);
this->enqueue(type, value);
this->enqueue(CommandType::COMMAND_TYPE_SAVE_CFG);
if (was_active)
this->enqueue(CommandType::COMMAND_TYPE_POWER, true);
}
bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
// The interval between two commands must be larger than the command duration
const uint32_t duration = cmd.type == CommandType::COMMAND_TYPE_SAVE_CFG ? 3000 : 1000;
if (!this->cmd_sent_) {
if (now - this->ts_last_cmd_sent_ > duration) {
char buf[64]; // "detRangeCfg -1" + 8 * " 32767" + null
this->format_command(cmd, buf, sizeof(buf));
ESP_LOGD(TAG, "Sending command: %s", buf);
this->write_str(buf);
this->ts_last_cmd_sent_ = now;
this->cmd_sent_ = true;
}
return false; // Command not done yet
}
if (const char *message = this->read_message_()) {
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
if (!this->handle_reply_(cmd, message))
return false; // Command not done yet
this->read_message_(); // Skip the prompt
return true;
}
ESP_LOGD(TAG, "Command not recognized properly by sensor");
} else if (now - this->ts_last_cmd_sent_ <= duration + 500) {
return false; // Command not done yet
} else {
ESP_LOGD(TAG, "Command timeout");
}
if (this->retries_left_ == 0) {
this->read_message_(); // Skip the prompt
return true; // Give up
}
this->retries_left_--;
this->cmd_sent_ = false;
ESP_LOGD(TAG, "Retrying");
return false;
}
void DfrobotSen0395Component::read_state_() {
const char *message = this->read_message_();
if (message == nullptr)
return;
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
this->set_detected_(false);
this->set_active(true);
} else if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
this->set_detected_(true);
this->set_active(true);
}
}
static size_t append_cfg_key(char *buf, size_t size, size_t pos) {
return buf_append_str(buf, size, pos, " 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89");
}
void DfrobotSen0395Component::format_command(const Command &cmd, char *buf, size_t size) const {
buf[0] = '\0';
switch (cmd.type) {
case CommandType::COMMAND_TYPE_POWER:
if (cmd.arg) {
buf_append_str(buf, size, 0, "sensorStart");
} else {
buf_append_str(buf, size, 0, "sensorStop");
}
break;
case CommandType::COMMAND_TYPE_DET_RANGE_CFG: {
size_t pos = buf_append_str(buf, size, 0, "detRangeCfg -1");
for (size_t i = 0; i < 8 && this->det_range_steps_[i] >= 0; i += 2) {
pos = buf_append_printf(buf, size, pos, " %d %d", this->det_range_steps_[i], this->det_range_steps_[i + 1]);
}
break;
}
case CommandType::COMMAND_TYPE_SET_LATENCY:
buf_append_printf(buf, size, 0, "setLatency %.03f %.03f", this->latency_steps_[0] * LATENCY_STEP_S,
this->latency_steps_[1] * LATENCY_STEP_S);
break;
case CommandType::COMMAND_TYPE_SENSOR_CFG_START:
buf_append_printf(buf, size, 0, "sensorCfgStart %d", cmd.arg);
break;
case CommandType::COMMAND_TYPE_FACTORY_RESET:
append_cfg_key(buf, size, buf_append_str(buf, size, 0, "factoryReset"));
break;
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
buf_append_str(buf, size, 0, "resetSystem");
break;
case CommandType::COMMAND_TYPE_SAVE_CFG:
append_cfg_key(buf, size, buf_append_str(buf, size, 0, "saveCfg"));
break;
case CommandType::COMMAND_TYPE_LED_MODE:
buf_append_printf(buf, size, 0, "setLedMode 1 %d", cmd.arg ? 0 : 1);
break;
case CommandType::COMMAND_TYPE_UART_OUTPUT:
buf_append_printf(buf, size, 0, "setUartOutput 1 %d", cmd.arg ? 1 : 0);
break;
case CommandType::COMMAND_TYPE_SENSITIVITY:
buf_append_printf(buf, size, 0, "setSensitivity %d", cmd.arg);
break;
}
}
bool DfrobotSen0395Component::handle_reply_(const Command &cmd, const char *message) {
auto is = [message](ESPHOME_PGM_P text) { return ESPHOME_strcmp_P(message, text) == 0; };
// Commands with their own replies
switch (cmd.type) {
case CommandType::COMMAND_TYPE_POWER:
if (is(ESPHOME_PSTR("sensor stopped already"))) {
this->set_active(false);
ESP_LOGI(TAG, "Stopped sensor (already stopped)");
} else if (is(ESPHOME_PSTR("sensor started already"))) {
this->set_active(true);
ESP_LOGI(TAG, "Started sensor (already started)");
} else if (is(ESPHOME_PSTR("new parameter isn't save, can't startSensor"))) {
this->set_active(false);
ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)");
} else if (is(ESPHOME_PSTR("Done"))) {
this->set_active(cmd.arg);
ESP_LOGI(TAG, "%s", cmd.arg ? LOG_STR_LITERAL("Started sensor") : LOG_STR_LITERAL("Stopped sensor"));
} else {
return false;
}
return true;
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
if (!is(PROMPT))
return false;
ESP_LOGI(TAG, "Restarted sensor.");
return true;
case CommandType::COMMAND_TYPE_SAVE_CFG:
if (is(ESPHOME_PSTR("no parameter has changed"))) {
ESP_LOGI(TAG, "Not saving config (no parameter changed).");
} else if (is(ESPHOME_PSTR("Done"))) {
ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor.");
} else {
return false;
}
return true;
default:
break;
}
// Configuration commands, which need the sensor stopped
if (is(ESPHOME_PSTR("sensor is not stopped"))) {
ESP_LOGE(TAG, "Cannot %s. Sensor is not stopped!",
LOG_STR_ARG(BusyActions::get_log_str(static_cast<uint8_t>(cmd.type), 0)));
return true;
}
if (!is(ESPHOME_PSTR("Done")))
return false;
switch (cmd.type) {
case CommandType::COMMAND_TYPE_DET_RANGE_CFG:
ESP_LOGI(TAG, "Updated detection area config:");
for (size_t i = 0; i < 8 && this->det_range_steps_[i] >= 0; i += 2) {
ESP_LOGI(TAG, " Detection area %u from %.02fm to %.02fm.", static_cast<unsigned>(i / 2 + 1),
this->det_range_steps_[i] * DET_RANGE_STEP_M, this->det_range_steps_[i + 1] * DET_RANGE_STEP_M);
}
break;
case CommandType::COMMAND_TYPE_SET_LATENCY:
ESP_LOGI(TAG,
"Updated output latency config:\n"
" Signal that someone was detected is delayed by %.03f s.\n"
" Signal that nobody is detected anymore is delayed by %.03f s.",
this->latency_steps_[0] * LATENCY_STEP_S, this->latency_steps_[1] * LATENCY_STEP_S);
break;
case CommandType::COMMAND_TYPE_SENSOR_CFG_START:
this->set_start_after_boot(cmd.arg);
ESP_LOGI(TAG, "Updated sensor startup behavior: %s",
cmd.arg ? LOG_STR_LITERAL("sensor will start automatically after power-on.")
: LOG_STR_LITERAL("sensor needs to be started manually after power-on."));
break;
case CommandType::COMMAND_TYPE_FACTORY_RESET:
ESP_LOGI(TAG, "Sensor factory reset done.");
break;
case CommandType::COMMAND_TYPE_LED_MODE:
this->set_led_active(cmd.arg);
ESP_LOGI(TAG, "Set led mode done. %s",
cmd.arg ? LOG_STR_LITERAL("Sensor LED will blink.") : LOG_STR_LITERAL("Turned off LED."));
break;
case CommandType::COMMAND_TYPE_UART_OUTPUT:
this->set_uart_presence_active(cmd.arg);
ESP_LOGI(TAG, "Set uart mode done. %s",
cmd.arg ? LOG_STR_LITERAL("Presence information is sent via UART and GPIO.")
: LOG_STR_LITERAL("Presence information is only sent via GPIO."));
break;
case CommandType::COMMAND_TYPE_SENSITIVITY:
ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", cmd.arg);
break;
default:
break;
}
return true;
}
const char *DfrobotSen0395Component::read_message_() {
uint8_t DfrobotSen0395Component::read_message_() {
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);
if (byte == ASCII_CR)
continue;
if (byte >= 0x7F)
byte = '?'; // needs to be valid utf8 string for log functions.
const char *line = this->line_reader_.feed(byte);
if (line == nullptr && byte == '>' && this->line_reader_.length() == sizeof(PROMPT) - 1) {
line = this->line_reader_.partial();
this->line_reader_.reset();
this->read_buffer_[this->read_pos_] = byte;
if (this->read_pos_ == 9 && byte == '>')
this->read_buffer_[++this->read_pos_] = ASCII_LF;
if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
this->read_buffer_[this->read_pos_] = 0;
this->read_pos_ = 0;
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
return 1; // Full message in buffer
} else {
this->read_pos_++;
}
if (line != nullptr) {
ESP_LOGV(TAG, "Message: %s", line);
return line;
}
this->line_reader_.log_if_overflowed(TAG);
}
return nullptr;
return 0; // No full message yet
}
uint8_t DfrobotSen0395Component::find_prompt_() {
if (this->read_message_()) {
std::string message(this->read_buffer_);
if (message.rfind("leapMMW:/>") != std::string::npos) {
return 1; // Prompt found
}
}
return 0; // Not found yet
}
uint8_t DfrobotSen0395Component::send_cmd_(const char *cmd, uint32_t duration) {
// The interval between two commands must be larger than the specified duration (in ms).
if (millis() - ts_last_cmd_sent_ > duration) {
this->write_str(cmd);
ts_last_cmd_sent_ = millis();
return 1; // Command sent
}
// Could not send command yet as command duration did not fully pass yet.
return 0;
}
void DfrobotSen0395Component::set_detected_(bool detected) {
@@ -328,4 +99,44 @@ void DfrobotSen0395Component::set_detected_(bool detected) {
#endif
}
int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
if (this->is_full()) {
ESP_LOGE(TAG, "Command queue is full");
return -1;
} else if (this->is_empty()) {
front_++;
}
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
return 1;
}
std::unique_ptr<Command> CircularCommandQueue::dequeue() {
if (this->is_empty())
return nullptr;
std::unique_ptr<Command> dequeued_cmd = std::move(commands_[front_]);
if (front_ == rear_) {
front_ = -1;
rear_ = -1;
} else {
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
}
return dequeued_cmd;
}
bool CircularCommandQueue::is_empty() { return front_ == -1; }
bool CircularCommandQueue::is_full() { return (rear_ + 1) % COMMAND_QUEUE_SIZE == front_; }
// Run execute method of first in line command.
// Execute is non-blocking and has to be called until it returns 1.
uint8_t CircularCommandQueue::process(DfrobotSen0395Component *parent) {
if (!is_empty()) {
return commands_[front_]->execute(parent);
} else {
return 1;
}
}
} // namespace esphome::dfrobot_sen0395
@@ -3,8 +3,6 @@
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/line_reader.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
@@ -13,30 +11,29 @@
#include "esphome/components/switch/switch.h"
#endif
#include "commands.h"
namespace esphome::dfrobot_sen0395 {
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 253; // largest LineReader size with a one byte position
static constexpr uint8_t COMMAND_QUEUE_SIZE = 20;
const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
// Keep in sync with the BusyActions table in dfrobot_sen0395.cpp
enum class CommandType : uint8_t {
COMMAND_TYPE_POWER,
COMMAND_TYPE_DET_RANGE_CFG,
COMMAND_TYPE_SET_LATENCY,
COMMAND_TYPE_SENSOR_CFG_START,
COMMAND_TYPE_FACTORY_RESET,
COMMAND_TYPE_RESET_SYSTEM,
COMMAND_TYPE_SAVE_CFG,
COMMAND_TYPE_LED_MODE,
COMMAND_TYPE_UART_OUTPUT,
COMMAND_TYPE_SENSITIVITY,
};
// forward declaration due to circular dependency
class DfrobotSen0395Component;
/// A queued command. arg is the on/off flag, startup mode or sensitivity; the detection range and
/// latency values are held by the component.
struct Command {
CommandType type;
uint8_t arg;
static const uint8_t COMMAND_QUEUE_SIZE = 20;
class CircularCommandQueue {
public:
int8_t enqueue(std::unique_ptr<Command> cmd);
std::unique_ptr<Command> dequeue();
bool is_empty();
bool is_full();
uint8_t process(DfrobotSen0395Component *parent);
protected:
int front_{-1};
int rear_{-1};
std::unique_ptr<Command> commands_[COMMAND_QUEUE_SIZE];
};
class DfrobotSen0395Component final : public uart::UARTDevice, public Component {
@@ -96,21 +93,9 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
}
#endif
// Commands run one at a time from a queue, at least 1 s apart, so loop() never blocks
bool enqueue(CommandType type, uint8_t arg = 0);
/// True when count more commands fit; logs when they do not, so a stop/start sequence is never queued in part
bool has_queue_room(size_t count) const;
/// Queue a detection range config; segments are used up to the first one with a negative bound
void enqueue_det_range_cfg(float min1, float max1, float min2, float max2, float min3, float max3, float min4,
float max4);
/// Queue an output latency config, both delays in seconds
void enqueue_set_latency(float delay_after_detection, float delay_after_disappear);
/// Queue a setting change, stopping the sensor around it and saving it when the sensor is running
void enqueue_saved_setting(CommandType type, bool value);
int8_t enqueue(std::unique_ptr<Command> cmd);
/// Queue a sensor reset
void reset_system() { this->enqueue(CommandType::COMMAND_TYPE_RESET_SYSTEM); }
/// Write the text sent to the sensor for cmd into buf
void format_command(const Command &cmd, char *buf, size_t size) const;
void reset_system() { this->enqueue(make_unique<ResetSystemCommand>()); }
protected:
#ifdef USE_BINARY_SENSOR
@@ -122,24 +107,19 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
bool led_active_{false};
bool uart_presence_active_{false};
bool start_after_boot_{false};
LineReader<MMWAVE_READ_BUFFER_LENGTH> line_reader_;
bool cmd_sent_{false};
uint8_t retries_left_{2};
StaticRingBuffer<Command, COMMAND_QUEUE_SIZE> cmd_queue_;
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
size_t read_pos_{0};
CircularCommandQueue cmd_queue_;
uint32_t ts_last_cmd_sent_{0};
// Range and latency of the queued commands, in sensor steps (0.15 m, 0.025 s); a later command overwrites them
int16_t det_range_steps_[8]{};
uint16_t latency_steps_[2]{};
/// The next complete line, valid until the next call, or nullptr
const char *read_message_();
void read_state_();
/// Run the command at the front of the queue; true when it is done
bool run_command_(const Command &cmd, uint32_t now);
/// Handle a reply to a sent command; true when the command is done
bool handle_reply_(const Command &cmd, const char *message);
uint8_t read_message_();
uint8_t find_prompt_();
uint8_t send_cmd_(const char *cmd, uint32_t duration);
void set_detected_(bool detected);
friend class Command;
friend class ReadStateCommand;
};
} // namespace esphome::dfrobot_sen0395
@@ -2,49 +2,63 @@ import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG
from esphome.cpp_generator import MockObjClass
from esphome.types import ConfigType
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component, dfrobot_sen0395_ns
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component
DEPENDENCIES = ["dfrobot_sen0395"]
CommandType = dfrobot_sen0395_ns.enum("CommandType", is_class=True)
dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395")
DfrobotSen0395Switch = dfrobot_sen0395_ns.class_(
"DfrobotSen0395Switch",
switch.Switch,
cg.Component,
cg.Parented.template(DfrobotSen0395Component),
)
_SWITCH_SCHEMA = switch.switch_schema(
switch.ApplySwitch,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend(
Sen0395PowerSwitch = dfrobot_sen0395_ns.class_(
"Sen0395PowerSwitch", DfrobotSen0395Switch
)
Sen0395LedSwitch = dfrobot_sen0395_ns.class_("Sen0395LedSwitch", DfrobotSen0395Switch)
Sen0395UartPresenceSwitch = dfrobot_sen0395_ns.class_(
"Sen0395UartPresenceSwitch", DfrobotSen0395Switch
)
Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_(
"Sen0395StartAfterBootSwitch", DfrobotSen0395Switch
)
def _switch_schema(class_: MockObjClass) -> cv.Schema:
return (
switch.switch_schema(
class_,
entity_category=ENTITY_CATEGORY_CONFIG,
)
.extend(
{
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(
DfrobotSen0395Component
),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.typed_schema(
{
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component),
"sensor_active": _switch_schema(Sen0395PowerSwitch),
"turn_on_led": _switch_schema(Sen0395LedSwitch),
"presence_via_uart": _switch_schema(Sen0395UartPresenceSwitch),
"start_after_boot": _switch_schema(Sen0395StartAfterBootSwitch),
}
)
# Per type: the hub setter that stores the switch, and the command its writes queue
_SWITCHES = {
"sensor_active": (
lambda hub: hub.set_sensor_active_switch,
f"enqueue({CommandType.COMMAND_TYPE_POWER}, state)",
),
"turn_on_led": (
lambda hub: hub.set_turn_on_led_switch,
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_LED_MODE}, state)",
),
"presence_via_uart": (
lambda hub: hub.set_presence_via_uart_switch,
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_UART_OUTPUT}, state)",
),
"start_after_boot": (
lambda hub: hub.set_start_after_boot_switch,
f"enqueue_saved_setting({CommandType.COMMAND_TYPE_SENSOR_CFG_START}, state)",
),
}
CONFIG_SCHEMA = cv.typed_schema(dict.fromkeys(_SWITCHES, _SWITCH_SCHEMA))
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID])
setter, apply = _SWITCHES[config[CONF_TYPE]]
await switch.sub_switches(config, parent=hub).create(
config, setter(hub), apply=apply
)
parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID])
var = await switch.new_switch(config)
await cg.register_component(var, config)
await cg.register_parented(var, parent)
cg.add(getattr(parent, f"set_{config[CONF_TYPE]}_switch")(var))
@@ -0,0 +1,46 @@
#include "dfrobot_sen0395_switch.h"
namespace esphome::dfrobot_sen0395 {
void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique<PowerCommand>(state)); }
void Sen0395LedSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<LedModeCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
void Sen0395UartPresenceSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<UartOutputCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
void Sen0395StartAfterBootSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
} // namespace esphome::dfrobot_sen0395
@@ -0,0 +1,32 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include "../dfrobot_sen0395.h"
namespace esphome::dfrobot_sen0395 {
class DfrobotSen0395Switch : public switch_::Switch, public Component, public Parented<DfrobotSen0395Component> {};
class Sen0395PowerSwitch final : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395LedSwitch final : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395UartPresenceSwitch final : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
class Sen0395StartAfterBootSwitch final : public DfrobotSen0395Switch {
public:
void write_state(bool state) override;
};
} // namespace esphome::dfrobot_sen0395
@@ -34,7 +34,10 @@ DFRobotSEN0465Component = dfrobot_sen0465_base_ns.class_(
"DFRobotSEN0465Component", cg.PollingComponent
)
AlarmType = dfrobot_sen0465_base_ns.enum("AlarmType", is_class=True)
ALARM_TYPES = AlarmType.options("LOW", "HIGH", prefix="ALARM_TYPE_")
ALARM_TYPES = {
"LOW": AlarmType.ALARM_TYPE_LOW,
"HIGH": AlarmType.ALARM_TYPE_HIGH,
}
# One piece of the temperature compensation from the DFRobot_MultiGasSensor library:
# for low < T <= high, compensated = raw / (a * T + b) - (c * T + d)
@@ -72,7 +72,10 @@ void DFRobotSEN0465Component::dump_config() {
if (this->alarm_type_ == AlarmType::ALARM_TYPE_OFF) {
ESP_LOGCONFIG(TAG, " Alarm: off");
} else {
const float threshold = this->alarm_threshold_ / pow10_int(this->alarm_decimals_);
float scale = 1.0f;
for (uint8_t i = 0; i < this->alarm_decimals_; i++)
scale *= 10.0f;
const float threshold = this->alarm_threshold_ / scale;
ESP_LOGCONFIG(TAG, " Alarm: %s %.*f",
this->alarm_type_ == AlarmType::ALARM_TYPE_HIGH ? LOG_STR_LITERAL("above") : LOG_STR_LITERAL("below"),
this->alarm_decimals_, threshold);
@@ -80,6 +83,9 @@ void DFRobotSEN0465Component::dump_config() {
if (this->setup_incomplete_) {
ESP_LOGCONFIG(TAG, " Setup incomplete: yes");
}
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Concentration", this->concentration_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
@@ -222,14 +228,23 @@ void DFRobotSEN0465Component::handle_data_(const uint8_t *frame) {
return;
}
if (frame[5] > 2) {
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
if (this->concentration_sensor_ != nullptr)
this->concentration_sensor_->publish_state(NAN);
this->status_set_warning();
return;
float concentration = encode_uint16(frame[2], frame[3]);
switch (frame[5]) {
case 0:
break;
case 1:
concentration *= 0.1f;
break;
case 2:
concentration *= 0.01f;
break;
default:
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
if (this->concentration_sensor_ != nullptr)
this->concentration_sensor_->publish_state(NAN);
this->status_set_warning();
return;
}
float concentration = encode_uint16(frame[2], frame[3]) * pow10_int(-frame[5]);
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
if (!this->compensation_.empty())
@@ -1,7 +1,10 @@
#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;
@@ -15,21 +18,30 @@ void DFRobotSEN0465UARTComponent::setup() {
}
void DFRobotSEN0465UARTComponent::loop() {
// 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)
// 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)
return;
this->rx_count_ = 0;
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
this->disable_loop();
this->handle_response_(frame);
this->handle_response_(this->rx_buffer_);
}
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->reader_.reset();
this->rx_count_ = 0;
this->write_array(frame, FRAME_SIZE);
return true;
}
@@ -38,7 +50,7 @@ void DFRobotSEN0465UARTComponent::read_response() {
this->enable_loop();
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
this->disable_loop();
this->reader_.reset();
this->rx_count_ = 0;
this->handle_no_response_();
});
}
@@ -2,7 +2,6 @@
#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 {
@@ -18,7 +17,8 @@ class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSE
bool write_frame(const uint8_t *frame) override;
void read_response() override;
FrameReader<dfrobot_sen0465_base::FRAME_SIZE, dfrobot_sen0465_base::FRAME_HEAD> reader_;
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
uint8_t rx_count_{0};
};
} // namespace esphome::dfrobot_sen0465_uart
@@ -3,7 +3,8 @@ from collections.abc import Callable
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_MODE, CONF_MODEL
from esphome.const import CONF_ID, CONF_MODE, CONF_MODEL
from esphome.core import ID
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -58,11 +59,16 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
)
def get_hub_config(root: ConfigType, hub_id: ID) -> ConfigType:
"""Return the configuration of the hub with the given id."""
return next(conf for conf in root[DOMAIN] if conf[CONF_ID] == hub_id)
def final_validate_mode(keys: dict[str, str]) -> Callable[[ConfigType], None]:
"""Reject entity keys that need a different hub mode; `keys` maps each key to the mode it needs."""
def validator(config: ConfigType) -> None:
hub = fv.get_declaring_config(config[CONF_DFROBOT_SEN0609_ID])
hub = get_hub_config(fv.full_config.get(), config[CONF_DFROBOT_SEN0609_ID])
for key, mode in keys.items():
if key in config and hub[CONF_MODE] != mode:
raise cv.Invalid(
@@ -2,19 +2,44 @@ import esphome.codegen as cg
from esphome.components import button
from esphome.components.const import CONF_QUERY_PARAMS
import esphome.config_validation as cv
from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_RESTART
from esphome.const import (
CONF_FACTORY_RESET,
CONF_ID,
CONF_RESTART,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_DATABASE,
ICON_RESTART,
ICON_RESTART_ALERT,
)
from esphome.types import ConfigType
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns
DFRobotSEN0609Button = dfrobot_sen0609_ns.class_("DFRobotSEN0609Button", button.Button)
ButtonAction = dfrobot_sen0609_ns.enum("ButtonAction")
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
button.ApplyButton
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
DFRobotSEN0609Button,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_QUERY_PARAMS): button.button_schema(
DFRobotSEN0609Button,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_DATABASE,
),
cv.Optional(CONF_RESTART): button.button_schema(
DFRobotSEN0609Button,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
),
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
}
@@ -22,7 +47,11 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
CONF_FACTORY_RESET,
hub.set_factory_reset_button,
ButtonAction.BUTTON_FACTORY_RESET,
)
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="query_params()")
await buttons(CONF_RESTART, hub.set_restart_button, apply="restart()")
await buttons(
CONF_QUERY_PARAMS, hub.set_query_button, ButtonAction.BUTTON_QUERY_PARAMS
)
await buttons(CONF_RESTART, hub.set_restart_button, ButtonAction.BUTTON_RESTART)
@@ -0,0 +1,30 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Button final : public button::Button, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Button(ButtonAction action) : action_(action) {}
protected:
void press_action() override {
switch (this->action_) {
case BUTTON_RESTART:
this->parent_->restart();
break;
case BUTTON_FACTORY_RESET:
this->parent_->factory_reset();
break;
case BUTTON_QUERY_PARAMS:
this->parent_->query_params();
break;
}
}
ButtonAction action_;
};
} // namespace esphome::dfrobot_sen0609
@@ -5,6 +5,7 @@
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -94,6 +95,12 @@ static bool answer_means_done(Operation operation, const char *line) {
}
}
static bool parse_float(const char *str, float &value) {
char *end;
value = strtof(str, &end);
return end != str && *end == '\0';
}
static const LogString *operation_to_str(Operation operation) {
switch (operation) {
case Operation::OPERATION_STOP:
@@ -131,9 +138,9 @@ void DFRobotSEN0609Component::dump_config() {
LOG_BINARY_SENSOR(" ", "HasTarget", this->has_target_binary_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR_WITH_DEDUP(" ", "TargetDistance", this->target_distance_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "TargetSpeed", this->target_speed_sensor_);
LOG_SENSOR_WITH_DEDUP(" ", "TargetEnergy", this->target_energy_sensor_);
LOG_SENSOR(" ", "TargetDistance", this->target_distance_sensor_);
LOG_SENSOR(" ", "TargetSpeed", this->target_speed_sensor_);
LOG_SENSOR(" ", "TargetEnergy", this->target_energy_sensor_);
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
@@ -166,19 +173,29 @@ void DFRobotSEN0609Component::setup() {
switch_::Switch *sw = this->switches_[i];
if (sw == nullptr)
continue;
// A disabled restore mode leaves the switch on, which an inverted switch writes as off
this->publish_switch_(static_cast<SwitchSetting>(i), sw->get_initial_write_state().value_or(!sw->is_inverted()));
const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
}
#endif
this->queue_configuration_();
}
void DFRobotSEN0609Component::loop() {
if (this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); })) {
uint8_t buf[MAX_LINE_LENGTH];
size_t avail = this->available();
while (avail > 0) {
const size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
if (!this->read_failed_) {
ESP_LOGW(TAG, "UART read failed");
}
this->read_failed_ = true;
break;
}
this->read_failed_ = false;
} else if (!this->read_failed_) {
ESP_LOGW(TAG, "UART read failed");
this->read_failed_ = true;
avail -= to_read;
for (size_t i = 0; i < to_read; i++)
this->readline_(buf[i]);
}
this->run_sequencer_();
}
@@ -269,6 +286,9 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
return;
}
char *fields[FRAME_FIELDS];
float distance;
float speed;
float energy;
if (split_fields(line + 7, fields, FRAME_FIELDS) < FRAME_FIELDS) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
@@ -288,10 +308,7 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
#endif
return;
}
const optional<float> distance = parse_number<float>(fields[2]);
const optional<float> speed = parse_number<float>(fields[3]);
const optional<float> energy = parse_number<float>(fields[4]);
if (!distance.has_value() || !speed.has_value() || !energy.has_value()) {
if (!parse_float(fields[2], distance) || !parse_float(fields[3], speed) || !parse_float(fields[4], energy)) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
}
@@ -300,7 +317,7 @@ void DFRobotSEN0609Component::handle_frame_(char *line) {
this->has_target_binary_sensor_->publish_state(true);
#endif
#ifdef USE_SENSOR
this->publish_target_(*distance, *speed, *energy);
this->publish_target_(distance, speed, energy);
#endif
}
@@ -313,18 +330,17 @@ void DFRobotSEN0609Component::log_bad_frame_(const LogString *problem, const cha
}
#ifdef USE_SENSOR
static void publish_deduplicated(sensor::SensorWithDedup<float> &sens, float value) {
if (std::isnan(value)) {
sens.publish_state_unknown();
} else {
sens.publish_state_if_not_dup(value);
}
static void publish_deduplicated(sensor::Sensor *sens, Deduplicator<float> &dedup, float value) {
if (sens == nullptr)
return;
if (std::isnan(value) ? dedup.next_unknown() : dedup.next(value))
sens->publish_state(value);
}
void DFRobotSEN0609Component::publish_target_(float distance, float speed, float energy) {
publish_deduplicated(this->target_distance_sensor_, distance);
publish_deduplicated(this->target_speed_sensor_, speed);
publish_deduplicated(this->target_energy_sensor_, energy);
publish_deduplicated(this->target_distance_sensor_, this->distance_dedup_, distance);
publish_deduplicated(this->target_speed_sensor_, this->speed_dedup_, speed);
publish_deduplicated(this->target_energy_sensor_, this->energy_dedup_, energy);
}
#endif
@@ -443,15 +459,6 @@ void DFRobotSEN0609Component::control_number(NumberSetting setting, float value)
this->request_write_(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[setting])));
}
void DFRobotSEN0609Component::control_number(number::Number *number, float value) {
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
if (this->numbers_[i] == number) {
this->control_number(static_cast<NumberSetting>(i), value);
return;
}
}
}
void DFRobotSEN0609Component::forget_number_changes_(Param param, bool unknown) {
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
if (progmem_read_byte(&NUMBER_PARAMS[i]) != param)
@@ -7,7 +7,7 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor_with_dedup.h"
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
@@ -62,6 +62,12 @@ enum SwitchSetting : uint8_t {
SWITCH_COUNT,
};
enum ButtonAction : uint8_t {
BUTTON_RESTART,
BUTTON_FACTORY_RESET,
BUTTON_QUERY_PARAMS,
};
/// Module parameters; a write or read of each is one command. Pending work runs in this order.
enum Param : uint8_t {
PARAM_RUN_APP,
@@ -97,9 +103,9 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
SUB_BINARY_SENSOR(has_target)
#endif
#ifdef USE_SENSOR
SUB_SENSOR_WITH_DEDUP(target_distance, float)
SUB_SENSOR_WITH_DEDUP(target_speed, float)
SUB_SENSOR_WITH_DEDUP(target_energy, float)
SUB_SENSOR(target_distance)
SUB_SENSOR(target_speed)
SUB_SENSOR(target_energy)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
@@ -124,8 +130,6 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
#ifdef USE_NUMBER
void set_number(NumberSetting setting, number::Number *number) { this->numbers_[setting] = number; }
void control_number(NumberSetting setting, float value);
// Finds the setting by pointer so every number shares one Apply function
void control_number(number::Number *number, float value);
#endif
#ifdef USE_SWITCH
void set_switch(SwitchSetting setting, switch_::Switch *sw) { this->switches_[setting] = sw; }
@@ -171,6 +175,9 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
#endif
#ifdef USE_SENSOR
void publish_target_(float distance, float speed, float energy);
Deduplicator<float> distance_dedup_;
Deduplicator<float> speed_dedup_;
Deduplicator<float> energy_dedup_;
#endif
#ifdef USE_NUMBER
@@ -18,7 +18,6 @@ from esphome.const import (
UNIT_SECOND,
)
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
from .. import (
@@ -29,8 +28,10 @@ from .. import (
DFRobotSEN0609Component,
dfrobot_sen0609_ns,
final_validate_mode,
get_hub_config,
)
DFRobotSEN0609Number = dfrobot_sen0609_ns.class_("DFRobotSEN0609Number", number.Number)
NumberSetting = dfrobot_sen0609_ns.enum("NumberSetting")
CONF_HOLD_SENSITIVITY = "hold_sensitivity"
@@ -54,7 +55,7 @@ class Setting(NamedTuple):
def _distance(icon: str = ICON_RULER) -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=icon,
@@ -64,7 +65,7 @@ def _distance(icon: str = ICON_RULER) -> cv.Schema:
def _duration() -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
@@ -74,7 +75,7 @@ def _duration() -> cv.Schema:
def _plain() -> cv.Schema:
return number.number_schema(
number.ApplyNumber,
DFRobotSEN0609Number,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
)
@@ -180,14 +181,14 @@ FINAL_VALIDATE_SCHEMA = final_validate_mode(
async def to_code(config: ConfigType) -> None:
hub_id = config[CONF_DFROBOT_SEN0609_ID]
hub = await cg.get_variable(hub_id)
max_range = MAX_RANGES[fv.get_declaring_config(hub_id, CORE.config)[CONF_MODEL]]
max_range = MAX_RANGES[get_hub_config(CORE.config, hub_id)[CONF_MODEL]]
numbers = number.sub_numbers(config, parent=hub)
for s in SETTINGS:
await numbers(
s.key,
lambda n, setting=s.setting: hub.set_number(setting, n),
s.setting,
min_value=s.min_value,
max_value=max_range if s.max_value is None else s.max_value,
step=s.step,
apply="control_number(entity, value)",
)
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Number final : public number::Number, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Number(NumberSetting setting) : setting_(setting) {}
protected:
void control(float value) override { this->parent_->control_number(this->setting_, value); }
NumberSetting setting_;
};
} // namespace esphome::dfrobot_sen0609
+5 -32
View File
@@ -1,7 +1,6 @@
from dataclasses import dataclass
from typing import Any, Protocol
from esphome import automation, core, pins
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components.const import (
@@ -14,7 +13,6 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_FROM,
CONF_HEIGHT,
CONF_ID,
@@ -28,7 +26,7 @@ from esphome.const import (
SCHEDULER_DONT_RUN,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.final_validate import full_config, get_declaring_config
from esphome.final_validate import full_config
from esphome.types import ConfigType
DOMAIN = "display"
@@ -56,33 +54,6 @@ DISPLAY_ROTATIONS = {
}
class DisplayModel(Protocol):
"""The parts of a display model used by enable_pin_schema()."""
def get_default(self, key: str, fallback: Any = ...) -> Any: ...
def enable_pin_schema(
model: DisplayModel, config: ConfigType
) -> dict[cv.Optional, Any]:
"""
Get the schema entry for a display's enable pins.
Pins given in the user config are used as they are. Otherwise the model's default enable
pins are used, except any that is also used elsewhere in the config, so a user can take
control of that pin, e.g. with a switch.
:param config: The display config as given by the user
"""
schema = cv.ensure_list(pins.gpio_output_pin_schema)
default = model.get_default(CONF_ENABLE_PIN, None)
if default is None or CONF_ENABLE_PIN in config:
return {cv.Optional(CONF_ENABLE_PIN): schema}
return {
cv.Optional(
CONF_ENABLE_PIN, default=default
): pins.PIN_SCHEMA_REGISTRY.optional(schema)
}
def validate_rotation(value):
value = cv.string(value)
value = value.removesuffix("°")
@@ -225,7 +196,9 @@ def get_display_metadata(display_id: ID) -> DisplayMetaData:
return meta
# No metadata found, display driver may not yet support it.
# Read the raw config to populate the returned data
disp_config = get_declaring_config(display_id)
global_config = full_config.get()
path = global_config.get_path_for_id(display_id)[:-1]
disp_config = global_config.get_config_for_path(path)
dimensions = disp_config.get(CONF_DIMENSIONS, (0, 0))
if isinstance(dimensions, dict):
dimensions = (dimensions.get(CONF_WIDTH, 0), dimensions.get(CONF_HEIGHT, 0))
+36 -15
View File
@@ -1,5 +1,7 @@
#include "ds1603l.h"
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -8,28 +10,35 @@ namespace esphome::ds1603l {
ESPHOME_LOG_TAG(TAG, "ds1603l.sensor");
void DS1603L::loop() {
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;
// 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;
}
if (!this->parse_data_(frame)) {
this->rx_buffer_[this->rx_count_++] = byte;
if (this->rx_count_ < FRAME_SIZE) {
continue;
}
if (this->parse_data_()) {
this->rx_count_ = 0;
} else {
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
this->reader_.resync();
this->resync_();
}
});
}
}
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
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];
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];
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
@@ -44,4 +53,16 @@ bool DS1603L::parse_data_(const uint8_t *frame) {
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
+6 -4
View File
@@ -6,7 +6,6 @@
#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 {
@@ -19,10 +18,13 @@ 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 and publishes it. Returns false if the frame is invalid.
bool parse_data_(const uint8_t *frame);
// 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_();
FrameReader<FRAME_SIZE, HEADER_BYTE> reader_;
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
};
} // namespace esphome::ds1603l
@@ -1,6 +1,5 @@
#include "ds248x_one_wire_bus.h"
#include "ds248x.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ds248x {
@@ -152,7 +151,11 @@ uint64_t DS248xOneWireBus::search_int() {
}
// Update address based on direction taken
assign_bits(address, bit_mask, dir_taken);
if (dir_taken) {
address |= bit_mask;
} else {
address &= ~bit_mask;
}
}
// Search successful
+3 -1
View File
@@ -32,7 +32,9 @@ CONFIG_SCHEMA = cv.Schema(
def _final_validate(config: ConfigType) -> None:
"""Validate that the channel is within the parent's channel count."""
parent_config = fv.get_declaring_config(config[CONF_DS248X_ID])
fconf = fv.full_config.get()
path = fconf.get_path_for_id(config[CONF_DS248X_ID])[:-1]
parent_config = fconf.get_config_for_path(path)
channel_count = get_channel_count(parent_config)
channel = config[CONF_CHANNEL]
+6 -4
View File
@@ -18,11 +18,13 @@ E131AddressableLightEffect = e131_ns.class_(
)
E131Component = e131_ns.class_("E131Component", cg.Component)
E131ListenMethod = e131_ns.enum("E131ListenMethod")
METHODS = E131ListenMethod.options("UNICAST", "MULTICAST", prefix="E131_")
METHODS = {"UNICAST": e131_ns.E131_UNICAST, "MULTICAST": e131_ns.E131_MULTICAST}
E131LightChannels = e131_ns.enum("E131LightChannels")
CHANNELS = E131LightChannels.options("MONO", "RGB", "RGBW", prefix="E131_")
CHANNELS = {
"MONO": e131_ns.E131_MONO,
"RGB": e131_ns.E131_RGB,
"RGBW": e131_ns.E131_RGBW,
}
CONF_UNIVERSE = "universe"
CONF_E131_ID = "e131_id"
Loaded 100 of 1032 files, more files were not shown because too many files have changed in this diff. Show more