Skip to content

[api] Let the API server be disabled at boot - #70

Merged
zkoalexey merged 1 commit into
dev-jethubfrom
feature/api-enable-disable
Aug 17, 2026
Merged

[api] Let the API server be disabled at boot#70
zkoalexey merged 1 commit into
dev-jethubfrom
feature/api-enable-disable

Conversation

@zkoalexey

Copy link
Copy Markdown

Groundwork for jethome-iot/esphome-workspace#14 — "Возможность включать/отключать api сервер для связи с HA". The workspace PR that uses this is opened separately and pins this branch.

What

APIServer gains set_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, and disable_loop()s; dump_config() says Server: 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, and App.reboot() would fire every reboot_timeout forever. (JetHome configs happen to set reboot_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:

  • mDNS no longer publishes _esphomelib when the server is disabled. Its "publish _http so that .local still 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.
  • MQTT discovery omits port and the api_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 a enable_on_boot: config option; this does not. Nothing in the fork's configs needs one, and the only writer is jxd_config's network settings in the workspace repo, which applies the stored value from an apply component at setup_priority::WIFI + 1.0 — one step ahead of the API server's AFTER_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 flag setup() read (or will read). The single-writer-before-setup contract is documented on the setter; mDNS at AFTER_CONNECTION and MQTT discovery both run after the API's setup(), 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:

result
fresh device, flag default API up, 6053 answers a plaintext HelloRequest
flag stored false, power cycle [C][api]: Disabled by configuration, 6053 → connection reset (guest not listening)
flag stored true again, power cycle 6053 answers again, [D][api]: Accept in the log

Note for anyone testing this under QEMU: App.safe_reboot() does not come back on this emulator — the app panics at PC 0x00000000 right after Disabling RNG early entropy source. That reproduces identically with a plain POST /api/device/system/reboot on 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() and dump_config() short-circuit when disabled (no socket, no reboot timeout, no controller callback wiring; log Server: disabled).
  • Gate mDNS _esphomelib publication and MQTT discovery port/api_encryption* fields on APIServer::is_enabled(), including a runtime mDNS fallback _http service 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.

@zkoalexey
zkoalexey force-pushed the feature/api-enable-disable branch from 730b19f to 0839b2c Compare August 17, 2026 09:34
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 port field: 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()) {

@zkoalexey
zkoalexey merged commit 56ef75a into dev-jethub Aug 17, 2026
49 checks passed
@zkoalexey
zkoalexey deleted the feature/api-enable-disable branch August 17, 2026 11:20
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