[api] Let the API server be disabled at boot - #70
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a runtime “enable on boot” switch to ESPHome’s native APIServer, allowing firmware (or another component configured earlier in the boot sequence) to prevent the API server from opening its listening socket and arming the API reboot timeout. It also updates outward advertisement mechanisms (mDNS and MQTT discovery) to avoid pointing Home Assistant/clients at a closed API port when the server is disabled.
Changes:
- Add
APIServer::set_enable_on_boot(bool)/APIServer::is_enabled()and store the flag on the server instance. - Make
APIServer::setup()anddump_config()short-circuit when disabled (no socket, no reboot timeout, no controller callback wiring; logServer: disabled). - Gate mDNS
_esphomelibpublication and MQTT discoveryport/api_encryption*fields onAPIServer::is_enabled(), including a runtime mDNS fallback_httpservice when the service list would otherwise be empty.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| esphome/components/api/api_server.h | Introduces enable-on-boot API and backing flag on APIServer. |
| esphome/components/api/api_server.cpp | Skips API server initialization and config output when disabled. |
| esphome/components/mdns/mdns_component.cpp | Stops advertising API mDNS service when disabled; adds runtime fallback service when needed. |
| esphome/components/mqtt/mqtt_client.cpp | Omits API discovery port and Noise encryption fields when the API server is disabled. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
730b19f to
0839b2c
Compare
A JetHome device can now be told to come up without its Home Assistant native API server, from the device's own web UI. The server binds its listening socket in setup() and has no runtime stop, so the switch is a boot-time one: APIServer gains set_enable_on_boot()/is_enabled(), and a disabled setup() returns before it creates the socket, arms the "api_reboot" timeout or registers per-entity state callbacks. The reboot timeout is the reason for returning that early rather than just skipping the socket: it is armed before the socket exists, and nothing would ever disarm it on a device no client can connect to, so the device would reboot every reboot_timeout forever. Two places advertise the API to the outside world and would otherwise keep pointing at a closed port: - mDNS no longer publishes _esphomelib when the server is disabled. Its "publish _http so that .local still resolves" fallback becomes a runtime check as well as a compile-time one, because a build whose only service is the API can now end up with an empty service list. - The MQTT discovery payload omits the API port and the api_encryption flags for the same reason. The flag has no YAML key: nothing in the fork's own configs sets it, and the only writer is jxd_config's network settings, which applies the stored value at setup_priority WIFI + 1.0 — one step ahead of the API server's AFTER_WIFI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0839b2c to
f983261
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (3)
esphome/components/mqtt/mqtt_client.cpp:165
- Same as the
portfield: if the API server failed during setup, publishing encryption capability here can mislead Home Assistant into trying the native API. Consider also gating on!is_failed().
if (api::global_api_server->is_enabled()) {
esphome/components/mqtt/mqtt_client.cpp:129
- This guard prevents MQTT discovery from advertising the API port when the server is intentionally disabled, but it can still advertise a closed port if the API server failed during setup (socket/bind/listen -> mark_failed()). Consider also checking
!is_failed()so discovery reflects actual availability.
This issue also appears on line 165 of the same file.
if (api::global_api_server->is_enabled()) {
esphome/components/mdns/mdns_component.cpp:63
- This skips advertising the API service when intentionally disabled, but if the API server fails during setup (socket/bind/listen -> mark_failed()) it will still be advertised and point clients at a closed port. Consider also checking
!is_failed()to reflect actual service availability.
if (api::global_api_server != nullptr && api::global_api_server->is_enabled()) {
Groundwork for jethome-iot/esphome-workspace#14 — "Возможность включать/отключать api сервер для связи с HA". The workspace PR that uses this is opened separately and pins this branch.
What
APIServergainsset_enable_on_boot(bool)/is_enabled(). When disabled,setup()returns before it creates the listening socket, arms the"api_reboot"timeout or registers per-entity state callbacks, anddisable_loop()s;dump_config()saysServer: disabled.Returning that early — rather than only skipping the socket — is deliberate.
schedule_reboot_timeout_()runs before the socket is created, and it is disarmed in exactly one place:loop(), when the first client connects. On a device no client can reach, nothing would ever disarm it, andApp.reboot()would fire everyreboot_timeoutforever. (JetHome configs happen to setreboot_timeout: 0min, so they would have survived it; other builds would not.)Two places advertise the API outward and would otherwise keep pointing at a closed port:
_esphomelibwhen the server is disabled. Its "publish_httpso that.localstill resolves" fallback had to become a runtime check as well as a compile-time one: a build whose only service is the API can now end up with an empty service list, which the old#if !defined(USE_API) && …guard could not see.portand theapi_encryption*flags for the same reason — otherwise Home Assistant discovers the device over MQTT and then fails to connect to 6053.What this deliberately does not add
No YAML key. wifi / ethernet / mqtt each pair
set_enable_on_boot()with aenable_on_boot:config option; this does not. Nothing in the fork's configs needs one, and the only writer isjxd_config's network settings in the workspace repo, which applies the stored value from an apply component atsetup_priority::WIFI + 1.0— one step ahead of the API server'sAFTER_WIFI. Easy to add later if a device ever wants to ship with the API off by default.is_enabled()describes intent, not live state. It is the flagsetup()read (or will read). The single-writer-before-setup contract is documented on the setter; mDNS atAFTER_CONNECTIONand MQTT discovery both run after the API'ssetup(), so they see a settled value.Verified
Built and booted on the QEMU emulator (
script/qemu.sh run jxd-r6-e1eth-lcd) through the full cycle, against the workspace branch that drives the flag:6053answers a plaintextHelloRequestfalse, power cycle[C][api]: Disabled by configuration,6053→ connection reset (guest not listening)trueagain, power cycle6053answers again,[D][api]: Acceptin the logNote for anyone testing this under QEMU:
App.safe_reboot()does not come back on this emulator — the app panics atPC 0x00000000right afterDisabling RNG early entropy source. That reproduces identically with a plainPOST /api/device/system/rebooton an unmodified build, so it is a pre-existing emulator limitation, not something this change introduces. Power-cycle the emulator instead.🤖 Generated with Claude Code