From 72a2a88fea4e2cd8b82b768625b545845348ef4c Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 19 Jun 2026 11:51:13 +0200 Subject: [PATCH] Add env var SETUP_PORT to change the default port (#174263) --- homeassistant/components/http/__init__.py | 5 +-- homeassistant/components/http/config.py | 26 ++++++++++++- homeassistant/components/http/const.py | 2 + tests/components/http/test_init.py | 47 ++++++++++++++++++++++- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 102985e6b149..85a4c3ead687 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -33,7 +33,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HASSIO_USER_NAME, - SERVER_PORT, ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -60,7 +59,7 @@ from homeassistant.util.json import json_loads from .auth import async_setup_auth from .ban import setup_bans -from .config import async_load_config +from .config import async_load_config, default_server_port from .const import ( # noqa: F401 CONF_BASE_URL, CONF_CORS_ORIGINS, @@ -109,7 +108,7 @@ HTTP_SCHEMA: Final = vol.All( vol.Optional(CONF_SERVER_HOST): vol.All( cv.ensure_list, vol.Length(min=1), [cv.string] ), - vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, + vol.Optional(CONF_SERVER_PORT, default=default_server_port): cv.port, vol.Optional(CONF_BASE_URL): cv.string, vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, diff --git a/homeassistant/components/http/config.py b/homeassistant/components/http/config.py index 066c3a371f2d..20952d51881c 100644 --- a/homeassistant/components/http/config.py +++ b/homeassistant/components/http/config.py @@ -3,6 +3,7 @@ import asyncio from ipaddress import IPv4Network, IPv6Network, ip_network import logging +import os from typing import Any, Final, TypedDict, cast, override import voluptuous as vol @@ -31,6 +32,7 @@ from .const import ( CONF_USE_X_FRAME_OPTIONS, DEFAULT_CORS, DOMAIN, + ENV_SETUP_PORT, NO_LOGIN_ATTEMPT_THRESHOLD, SSL_INTERMEDIATE, SSL_MODERN, @@ -38,6 +40,28 @@ from .const import ( _LOGGER = logging.getLogger(__name__) + +def default_server_port() -> int: + """Return the default HTTP server port. + + The built-in default port can be overridden via the + ``SETUP_PORT`` environment variable. An invalid value is ignored in favor + of the built-in default. + """ + if (env_value := os.environ.get(ENV_SETUP_PORT)) is None: + return SERVER_PORT + try: + return cast(int, cv.port(env_value)) + except vol.Invalid: + _LOGGER.warning( + "Invalid port %r in %s environment variable; falling back to %s", + env_value, + ENV_SETUP_PORT, + SERVER_PORT, + ) + return SERVER_PORT + + STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 2 @@ -86,7 +110,7 @@ HTTP_STORAGE_SCHEMA: Final = vol.Schema( vol.Optional(CONF_SERVER_HOST): vol.All( cv.ensure_list, vol.Length(min=1), [cv.string] ), - vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, + vol.Optional(CONF_SERVER_PORT, default=default_server_port): cv.port, vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index ae30fc9aecbb..7426737904ab 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -28,6 +28,8 @@ CONF_SSL_PROFILE: Final = "ssl_profile" SSL_MODERN: Final = "modern" SSL_INTERMEDIATE: Final = "intermediate" +ENV_SETUP_PORT: Final = "SETUP_PORT" + # Cast to be able to load custom cards. # My to be able to check url and version info. DEFAULT_CORS: Final[list[str]] = ["https://cast.home-assistant.io"] diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 6f6fad9545c9..6ca3a5056074 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -15,7 +15,12 @@ from homeassistant.auth.providers.homeassistant import HassAuthProvider from homeassistant.components import cloud, http from homeassistant.components.cloud import CloudNotAvailable from homeassistant.components.http import DOMAIN -from homeassistant.components.http.config import _DEFAULT_CONFIG, HTTP_STORAGE_SCHEMA +from homeassistant.components.http.config import ( + _DEFAULT_CONFIG, + HTTP_STORAGE_SCHEMA, + default_server_port, +) +from homeassistant.components.http.const import ENV_SETUP_PORT from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir @@ -1191,6 +1196,46 @@ async def test_setup_migrates_v1_storage_to_v2( assert data["yaml_migration_done"] is True +@pytest.mark.parametrize( + ("env", "expected_port"), + [ + pytest.param({}, 8123, id="unset"), + pytest.param({ENV_SETUP_PORT: "80"}, 80, id="valid"), + pytest.param({ENV_SETUP_PORT: "0"}, 8123, id="out-of-range"), + pytest.param({ENV_SETUP_PORT: "notaport"}, 8123, id="not-a-number"), + pytest.param({ENV_SETUP_PORT: ""}, 8123, id="empty"), + ], +) +def test_default_server_port( + env: dict[str, str], + expected_port: int, +) -> None: + """Test SETUP_PORT overrides the default port and invalid values fall back.""" + with patch.dict(os.environ, env, clear=True): + assert default_server_port() == expected_port + + +async def test_setup_port_env_var_used_as_default( + hass: HomeAssistant, + hass_storage: dict[str, Any], +) -> None: + """Test SETUP_PORT is used as the default server port without YAML config.""" + mock_server = Mock() + with ( + patch.dict(os.environ, {ENV_SETUP_PORT: "80"}), + patch( + "asyncio.BaseEventLoop.create_server", return_value=mock_server + ) as mock_create_server, + ): + assert await async_setup_component(hass, "http", {}) + await hass.async_start() + await hass.async_block_till_done() + + args, _ = mock_create_server.call_args + assert args[2] == 80 + assert hass_storage["http"]["data"]["pending"]["server_port"] == 80 + + async def test_websocket_http_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator,