Skip to content

Add InfluxDB3 writer with deconz support and tests to validate mappin…#9

Open
xlfe wants to merge 8 commits into
mainfrom
add-influxdb3-support
Open

Add InfluxDB3 writer with deconz support and tests to validate mappin…#9
xlfe wants to merge 8 commits into
mainfrom
add-influxdb3-support

Conversation

@xlfe

@xlfe xlfe commented Jan 31, 2026

Copy link
Copy Markdown
Owner

…g plus config/docs updates to clarify measurement and whitelist behavior

xlfe and others added 8 commits January 31, 2026 14:34
…g plus config/docs updates to clarify measurement and whitelist behavior
Queue writes off the main loop, tag/alias ESPHome props, and store deCONZ state under :radiale.deconz so domain filters line up.
 - Fix Python NaN/Inf filtering in pod.py using math.isnan()/isinf()
   instead of identity comparison which silently passed bad values

 - Prevent channel deadlocks by replacing blocking async/>!! with
   non-blocking async/offer! in state.clj and watch.clj, logging
   warnings when the channel is full instead of blocking the main loop

 - Add error handling and logging throughout:
   - Wrap ::fn handler invocation in try-catch (core.clj)
   - Add try-catch around watch handlers (watch.clj)
   - Log when events are filtered or channels uninitialized (influx.clj)

 - Optimize InfluxDB3 writes with time-based batching:
   - Accumulate points for 100ms before writing
   - Use writePoints() batch API instead of individual writePoint() calls
   - Enable noSync=true in WriteOptions to skip WAL persistence wait
   - Reduces write latency from ~1s per point to ~50ms per batch

 - Convert high-frequency debug logs to trace level for cleaner output
…oot)

Previously, on_disconnect returned early when expected=True, skipping the
reconnect loop entirely. Now it waits 30s for the device to reboot, then
enters the normal reconnect loop.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Generic primitives — extend!/clear!/bump!/bump-cmd — for any device that
should turn off after a deadline that multiple event sources can push
forward. Keyed by ::watchdog/key so multiple instances coexist in one
state* atom.

Also adds:

 - deconz/on-press: watch-handler factory for button-style sensors. Gates
   on :lastupdated and reads :buttonevent from @State*, fixing dropped
   repeat presses where clojure.data/diff strips :buttonevent from
   ::state/now because only :lastupdated changed.

 - schedule/cancel: kills a pending at-most-once job and clears its
   state-path entry. Used by watchdog/clear! so an off-button can stop a
   running watchdog without scheduling a new fire.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A self-rescheduling rc-fn that reads a watchdog's deadline, calls a
render function with minutes-remaining (or nil when the deadline is
gone), pushes the resulting commands to send-chan, and reschedules
itself for tick-seconds (default 60) later. Self-terminates when the
deadline is past.

Mechanism: the rescheduling schedule/after carries an ::rc/then that
back-references ticks via (assoc cfg ::rc/fn ticks). When the schedule
fires, try-fn merges clean-m with the ::rc/then map and dispatches
ticks again — the chain re-enters via the channel, not the stack.

Useful for any watchdog client that wants a recurring side-effect (a
keypad showing minutes-remaining, a periodic log line, a heartbeat).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
On a clean device reboot (expected disconnect), two reconnectors raced: the
mDNS-driven subscribe-esp* path and the on_disconnect retry loop. The loop slept
30s for the expected disconnect *before* claiming `connecting`, so it didn't
block the mDNS path; it then fired a second connect() that reassigned self.cli
and awaited APIClient.connect() with no timeout — stacking a duplicate
connection on a device that already had one. That await hung indefinitely.
Because `connecting` was reset only after the loop, the hang latched
connecting=True forever, wedging the mDNS fallback too, while self.cli (never
nulled on disconnect) let service_command fire execute_service into a dead
client and report success:true. A keypad stayed silently dead for ~34h until
radiale was restarted.

esphome.py:
- _close_cli(): null self.cli/self.connected before force-disconnecting, and
  call it before every connect() so we never stack a 2nd connection.
- connect(): wrap APIClient.connect() in asyncio.wait_for(CONNECT_TIMEOUT) so a
  stalled post-TCP handshake can't hang the reconnector.
- on_disconnect(): claim `connecting` before any await (closing the race
  window), reset it in finally (can never latch), and retry unbounded with
  capped exponential backoff (5..60s) instead of giving up after 15 tries.
  Refresh services on reconnect (parity with the mDNS path).
- _is_live() gate on command handlers: report success:false when not holding a
  live connection instead of firing into a corpse. (execute_service is one-way,
  so success:true still only means "delivered to a live connection", never "the
  device acted".)

pod.py: hold `connecting` across the subscribe-esp* connect with try/finally so
it and on_disconnect are mutually exclusive.

tests: update on_disconnect tests for unbounded/backoff behavior; gate command
tests on connected=True; add tests for _close_cli, _is_live, connect-timeout
teardown, and not-connected command handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… 45.x

aioesphomeapi flips its fire-and-forget commands between sync and async across
versions. radiale called them synchronously, which was correct for 42.x. The
build on `free` now runs 45.0.2, where execute_service is an `async def` — so
service_command created a coroutine and discarded it ("coroutine
APIClient.execute_service was never awaited"), logging success:true while the
keypad beeper/display services never reached the device. (switch_command,
light_command and send_home_assistant_state are still sync in 45.0.2; only
execute_service flipped, which is why keypad display *text* still worked.)

Route all four fire-and-forget commands through _maybe_await(), which awaits the
result iff it is awaitable — correct whether the installed aioesphomeapi makes
them sync or async, so a future version flip can't silently drop commands again.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants