mirror of
https://github.com/home-assistant/core.git
synced 2026-08-06 13:26:29 +01:00
146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
"""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"])
|