"""WebSocket API for the HTTP integration user config.""" from typing import Any, Final import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.homeassistant import ( DOMAIN as HASS_DOMAIN, SERVICE_HOMEASSISTANT_RESTART, ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from .config import HTTP_STORAGE_SCHEMA, ConfData, async_get_and_load_store from .const import ATTR_CONFIG, CONF_SERVER_PORT from .server import async_verify_can_bind ERR_BIND_FAILED: Final = "bind_failed" ERR_NOT_RUNNING: Final = "not_running" @callback def async_register_websocket_commands(hass: HomeAssistant) -> None: """Register the HTTP config websocket commands.""" websocket_api.async_register_command(hass, websocket_get_config) websocket_api.async_register_command(hass, websocket_set_config) websocket_api.async_register_command(hass, websocket_promote_config) @websocket_api.require_admin @websocket_api.websocket_command({vol.Required("type"): "http/config"}) @websocket_api.async_response async def websocket_get_config( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], ) -> None: """Return the HTTP configuration. ``stable`` is the confirmed-working config ``pending`` is an unconfirmed config awaiting promotion, or ``None``. A pending config that failed its trial is kept with its ``error`` (and ``error_message``) recorded, but is never applied again. ``revert_at`` is when an unconfirmed pending config auto-reverts to stable, or ``None`` when no revert is scheduled. ``active_config_type`` is the slot the running server was started with. ``default`` is the built-in default config. """ store = await async_get_and_load_store(hass) connection.send_result( msg["id"], { "stable": store.stable, "pending": store.pending, "revert_at": store.revert_deadline, "active_config_type": store.active_config_type, "default": store.default, }, ) @websocket_api.require_admin @websocket_api.websocket_command( { vol.Required("type"): "http/config/configure", vol.Required(ATTR_CONFIG): vol.Any(None, HTTP_STORAGE_SCHEMA), } ) @websocket_api.async_response async def websocket_set_config( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], ) -> None: """Store a new pending HTTP configuration and restart to apply it. Only allowed while Home Assistant is running: applying a config means restarting, and restarting a start that has not finished yet leaves integrations that are still setting up in an undefined state. A new config is first verified to be applicable by binding its configured address, so an unusable config is rejected here instead of being discovered after the restart. The check is skipped when the port matches the currently bound one: the running server holds that port until the restart releases it, so a probe would always fail against ourselves. Restart whenever the pending slot changes, so the runtime config is refreshed. The result reports whether a restart was triggered via ``{"restart": bool}``. """ if hass.state is not CoreState.running: connection.send_error( msg["id"], ERR_NOT_RUNNING, "The HTTP configuration can only be changed while Home Assistant " f"is running, current state: {hass.state.value}", ) return config: ConfData | None = msg[ATTR_CONFIG] if config is not None and config[CONF_SERVER_PORT] != hass.http.server_port: try: await async_verify_can_bind(hass, config) except HomeAssistantError as err: connection.send_error(msg["id"], ERR_BIND_FAILED, str(err)) return store = await async_get_and_load_store(hass) previous_pending = store.pending await store.async_set_pending(config) restart = store.pending != previous_pending connection.send_result(msg["id"], {"restart": restart}) if restart: await hass.services.async_call(HASS_DOMAIN, SERVICE_HOMEASSISTANT_RESTART) @websocket_api.require_admin @websocket_api.websocket_command({vol.Required("type"): "http/config/promote"}) @websocket_api.async_response async def websocket_promote_config( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], ) -> None: """Promote the pending HTTP config to stable. Called by the user after they have verified Home Assistant is working correctly with the pending config. The stable config is the one used by recovery mode, so promotion must be explicit. """ store = await async_get_and_load_store(hass) try: await store.async_promote_pending() except HomeAssistantError as err: connection.send_error( msg["id"], websocket_api.const.ERR_NOT_ALLOWED, str(err), ) return connection.send_result(msg["id"])