Skip to content

feat: run an external program for each reading (--exec) - #8

Merged
mreymann merged 2 commits into
mreymann:mainfrom
discostur:feat/exec-hook
Aug 4, 2026
Merged

mreymann merged 2 commits into
mreymann:mainfrom
discostur:feat/exec-hook

Conversation

@discostur

Copy link
Copy Markdown
Contributor

Closes the README item "call external scripts".

--exec /path/to/script runs a program once per reading. The reading is handed over twice so a script can use whichever is easier — as MITEMPR_* environment variables, and as one JSON object on stdin:

#!/bin/sh
[ -n "$MITEMPR_TEMPERATURE" ] || exit 0
echo "$MITEMPR_NAME is at ${MITEMPR_TEMPERATURE} C"

Every variable is always set, empty when the sensor did not report that measurement. Omitting them instead would let a value inherited from mitempr's own environment show through, and an empty string is easy to test for in shell.

Rate limiting, because a Pi Zero W has one core and sensors advertise every second or two:

  • --exec-interval SECS is the shortest gap between two runs for the same sensor, tracked per address. Default 0, i.e. every reading.
  • At most four hooks run at once. Beyond that, readings are dropped with a warning rather than queued: a backlog of stale temperatures is worse than a gap, and an unbounded queue on this hardware ends badly.
  • A hook that has not finished within 30 seconds is killed, so a stuck script cannot hold one of those four slots forever.

The child is spawned into its own task, so a slow script delays its own next run but never the scan.

Its stdout goes to /dev/null on purpose: with --format json, anything the script printed would otherwise interleave with the reading stream and corrupt it. stderr is inherited so a broken script can still complain visibly.

Five tests, including one end-to-end case that writes a real shell script to a temp directory and checks what it actually received on stdin and in its environment.


I went through the README TODO list and found a few real bugs on the way, so this
is one of ten PRs. They are meant to be reviewed and merged in this order, and
each is a separate branch so you can take some and leave others:

  1. chore/ci-deps-and-metadata — CI, unused deps, lockfile
  2. test/decoder-assertions — make the decoder tests actually assert
  3. fix/bthome-object-lengths — BTHome parser desync + encryption flag
  4. fix/mibeacon-frame-control — MiBeacon hardcoded offset + encryption flag
  5. fix/continuous-scan — one reading per sensor ever, and the watchdog loop
  6. feat/logging-and-output (draft) — log levels, --format json
  7. feat/config-file (draft) — name, calibrate and filter sensors
  8. feat/exec-hook (draft) — --exec
  9. feat/prometheus (draft) — --metrics-addr and --pushgateway-url
  10. feat/encrypted-payloads (draft) — AES-CCM bind keys

6–10 are drafts because they build on each other; GitHub can only point a
cross-fork PR at main, so until the ones before it are merged each diff also
shows its ancestors. I will rebase and force-push as things merge, so each
shrinks to its own change. Nothing here is urgent — take them at whatever pace
suits, or tell me to close some.

Everything is checked with cargo fmt --check, cargo clippy -D warnings and
cargo test in a rust:1.96 container, since bluer's bluetoothd feature is
Linux-only and I develop on a Mac.

Closes the README item "call external scripts".

--exec /path/to/script runs a program once per reading. The reading is handed
over twice so a script can use whichever is easier: as MITEMPR_* environment
variables, and as one JSON object on stdin.

    #!/bin/sh
    [ -n "$MITEMPR_TEMPERATURE" ] || exit 0
    echo "$MITEMPR_NAME is at ${MITEMPR_TEMPERATURE} C"

Every variable is always set, empty when the sensor did not report that
measurement. Omitting them instead would let a value inherited from mitempr's
own environment show through, and an empty string is easy to test for in shell.

Rate limiting, because a Pi Zero W has one core and sensors advertise every
second or two:

  - --exec-interval SECS is the shortest gap between two runs for the same
    sensor, tracked per address. Default 0, i.e. every reading.
  - At most four hooks run at once. Beyond that, readings are dropped with a
    warning rather than queued: a backlog of stale temperatures is worse than
    a gap, and an unbounded queue on this hardware ends badly.
  - A hook that has not finished within 30 seconds is killed, so a stuck script
    cannot hold one of those four slots forever.

The child is spawned into its own task, so a slow script delays its own next
run but never the scan.

Its stdout goes to /dev/null on purpose: with --format json, anything the
script printed would otherwise interleave with the reading stream and corrupt
it. stderr is inherited so a broken script can still complain visibly.

Five tests cover the environment mapping, the empty-variable rule, the
per-sensor interval, and one end-to-end case that writes a real shell script to
a temporary directory and checks what it actually received on stdin and in its
environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mreymann
mreymann marked this pull request as ready for review August 4, 2026 16:41
@mreymann
mreymann merged commit 01d5264 into mreymann:main Aug 4, 2026
0 of 3 checks passed
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