|
antenna-switcher-client 0.5.0
Async C++17 client for the antenna-switcher device over the ESPHome native API
|
An asynchronous, domain-specific C++17 client library for the antenna-switcher ESP32 device spoken over the ESPHome native API (TCP + protobuf, optionally Noise-encrypted).
It is a thin wrapper over esphome-api-client (esphome::api): the generic ESPHome client stays an internal dependency, and this library exposes only the antenna-switcher surface.
Looking for the command-line tool? See **`bin/README.md`** for the full
antenna-switcher-clireference (Docker, Homebrew, every command and flag).
Why this works:
Options carries everything needed to reach the device. Only host is required; port defaults to 6053 and an empty noise_psk means plaintext.AntennaSwitcher starts a background worker thread but does not connect yet.connect() blocks until the handshake, entity enumeration, and per-channel discovery finish, then returns. On any failure it throws — so the call belongs inside try/catch.setInput and the other actions are safe to call from your thread; internally they are marshalled onto the worker's event loop.state() returns a thread-safe snapshot you can read at any time after connect().Requires CMake ≥ 3.25 and a C++17 compiler. The library target is antenna_switcher::client and the public header is <antenna_switcher/client.hpp>.
Pin the release tarball with a checksum:
Install rules are generated only when esphome-api-client is provided as an installed package (find_package) rather than fetched from source. When the dependency is built from source (the default), ANTENNA_SWITCHER_INSTALL is auto-disabled because a source-built dependency cannot be re-exported. For FetchContent / add_subdirectory consumption this does not matter — you link the target directly.
CMAKE_CXX_STANDARD 17 / target_compile_features(... cxx_std_17).)esphome::api), resolved via FetchContent from the v0.5.0 release tarball. As of v0.3.0 the protobuf wire layer and the Noise crypto are carried in-tree, so the only thing fetched for the library is header-only Asio — there are no system protobuf/libsodium prerequisites.Connection settings passed to the controller's constructor.
The controller. It owns a background thread that runs the ESPHome event loop. Construction starts the thread; connect() performs the blocking handshake + discovery. It is non-copyable.
Lifetime: keep the AntennaSwitcher alive for as long as you use it; the destructor stops the worker thread. Do not keep references to a ChannelState past the call that produced it — copy what you need (it is a small value type).
Which of the two switchers a call targets. The enum value is the channel number used in the device's entity object-ids.
A snapshot of one channel's live state. Returned by state() and delivered to your onStateChanged callback.
Manual, Auto, Plan, or Unknown — the device's current operating mode for a channel.
A plan is an ordered list of steps. Each step either selects an input or waits a delay. TimeUnit is Ms (milliseconds) or Us (microseconds).
setInput, startAuto, runPlan, stop, setAngleOffset, state, isConnected) are safe to call from any thread; commands are marshalled onto the event loop.onStateChanged fires on the worker (loop) thread. Keep it short and do not call back into a blocking connect() / disconnect() from inside it.state() returns a mutex-guarded copy.Scenario: normal successful usage. After connect() returns, both channels already have a known snapshot.
Inputs are 1..10. Selecting an input puts the channel in manual mode.
Edge case: a selection of 1..9 inputs becomes an explicit cycle order; an empty or full (all ten) selection cycles every input and emits no CSV suffix. This is verified in the tests (StartAutoFullSelectionDropsCsv).
Pass repeat=false (the default) to run the plan once.
Unlike the other actions, this writes the device's number entity directly rather than sending a command string.
Scenario: event-driven usage. The callback uses an atomic because it runs on a different thread than main.
The wire strings are replicated verbatim from the device web UI and pinned by the offline tests. All command strings are written to the channel's text command entity; the angle offset is written to the number entity.
| Method | Command string | Example |
|---|---|---|
setInput(ch, n) | set:N | set:5 |
startAuto(ch, v, Ms, inputs) | auto:DELAY[:csv] | auto:100:1,2,3 |
startAuto(ch, v, Us, {}) | auto:DELAYu | auto:5000u |
runPlan(ch, steps, repeat) | plan:part,…[,r] | plan:1,s100,2,s50u,r |
stop(ch) | stop | stop |
setAngleOffset(ch, deg) | *(number entity)* | — |
Rules:
<v> for milliseconds or <v>u for microseconds. A selection of 1..9 inputs is appended as a CSV cycle order; an empty or full (10) selection cycles every input.N) or a delay (sV ms / sVu µs); append r to repeat.The pure builders behind these are available in antenna_switcher::detail (build_set_input, build_start_auto, build_run_plan, build_stop) and are unit-testable without a device.
The library reports errors with exceptions:
connect() throws on handshake failure, an authentication/encryption error (esphome::api::ApiError subclasses), or a std::runtime_error if an expected entity is missing after discovery. Always call it inside try/catch.setInput, etc.) marshal work onto the loop and do not block on a device response, so they generally do not throw synchronously; observe their effect through state() or onStateChanged.state() and isConnected() do not throw.Success and failure:
| API | Purpose | Notes |
|---|---|---|
Options | Connection settings | host required; port=6053; empty noise_psk ⇒ plaintext |
AntennaSwitcher(Options) | Construct controller | Starts worker thread; non-copyable |
connect() | Connect + discover entities | Blocks; throws on failure |
disconnect() | Stop and disconnect | — |
isConnected() | Connection status | Does not throw |
setInput(ch, n) | Select input 1..10 | Manual mode |
startAuto(ch, v, unit, inputs) | Auto-cycle | 1..9 inputs ⇒ explicit order; empty/10 ⇒ all |
runPlan(ch, steps, repeat) | Run an ordered plan | repeat defaults to false |
stop(ch) | Stop auto/plan | — |
setAngleOffset(ch, deg) | Set compass offset 0..359 | Writes the number entity |
state(ch) | Thread-safe snapshot | Returns a ChannelState by value |
onStateChanged(cb) | Register state listener | Fires on the worker thread |
| Example | Demonstrates |
|---|---|
examples/control.cpp | Connect, exercise set / auto / plan / stop / angle-offset on channel #1, and print each onStateChanged event |
Build and run it against a live device:
A ready-to-use CLI, antenna-switcher-cli, ships in [bin/](bin). It wraps this library and adds text/JSON output, a config of saved devices, a streaming watch mode, and an interactive live panel.
The CLI's own dependencies (CLI11, spdlog, nlohmann/json) are private to the tool and never reach the library target. **See `bin/README.md` for the complete command reference, flags, environment variables, output formats, and interactive-mode keys.**
The build is driven by CMake; a Makefile and CMakePresets.json memorize the common invocations.
…or with plain CMake / presets:
Contributions to the library are welcome! If you encounter any issues or have suggestions for improvements, please feel free to submit a pull request or open an issue on the project's repository.
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.