diff --git a/supervisor/api/homeassistant.py b/supervisor/api/homeassistant.py index 673869c7174..ff74ed5394a 100644 --- a/supervisor/api/homeassistant.py +++ b/supervisor/api/homeassistant.py @@ -18,6 +18,7 @@ ATTR_BLK_WRITE, ATTR_BOOT, ATTR_CPU_PERCENT, + ATTR_DUPLICATE_LOG_FILE, ATTR_IMAGE, ATTR_IP_ADDRESS, ATTR_JOB_ID, @@ -55,6 +56,7 @@ vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str), vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str), vol.Optional(ATTR_BACKUPS_EXCLUDE_DATABASE): vol.Boolean(), + vol.Optional(ATTR_DUPLICATE_LOG_FILE): vol.Boolean(), } ) @@ -112,6 +114,7 @@ async def info(self, request: web.Request) -> dict[str, Any]: ATTR_AUDIO_INPUT: self.sys_homeassistant.audio_input, ATTR_AUDIO_OUTPUT: self.sys_homeassistant.audio_output, ATTR_BACKUPS_EXCLUDE_DATABASE: self.sys_homeassistant.backups_exclude_database, + ATTR_DUPLICATE_LOG_FILE: self.sys_homeassistant.duplicate_log_file, } @api_process @@ -151,6 +154,9 @@ async def options(self, request: web.Request) -> None: ATTR_BACKUPS_EXCLUDE_DATABASE ] + if ATTR_DUPLICATE_LOG_FILE in body: + self.sys_homeassistant.duplicate_log_file = body[ATTR_DUPLICATE_LOG_FILE] + await self.sys_homeassistant.save_data() @api_process diff --git a/supervisor/const.py b/supervisor/const.py index 8b7c9cca940..d9f6431b185 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -179,6 +179,7 @@ ATTR_DOCKER_API = "docker_api" ATTR_DOCUMENTATION = "documentation" ATTR_DOMAINS = "domains" +ATTR_DUPLICATE_LOG_FILE = "duplicate_log_file" ATTR_ENABLE = "enable" ATTR_ENABLE_IPV6 = "enable_ipv6" ATTR_ENABLED = "enabled" diff --git a/supervisor/docker/const.py b/supervisor/docker/const.py index b5ccc5b031e..d9bcc31ae75 100644 --- a/supervisor/docker/const.py +++ b/supervisor/docker/const.py @@ -133,6 +133,7 @@ def from_status(cls, status: str) -> PullImageLayerStage | None: return None +ENV_DUPLICATE_LOG_FILE = "HA_DUPLICATE_LOG_FILE" ENV_TIME = "TZ" ENV_TOKEN = "SUPERVISOR_TOKEN" ENV_TOKEN_OLD = "HASSIO_TOKEN" diff --git a/supervisor/docker/homeassistant.py b/supervisor/docker/homeassistant.py index 27327190194..6581c4da444 100644 --- a/supervisor/docker/homeassistant.py +++ b/supervisor/docker/homeassistant.py @@ -14,6 +14,7 @@ from ..jobs.const import JobConcurrency from ..jobs.decorator import Job from .const import ( + ENV_DUPLICATE_LOG_FILE, ENV_TIME, ENV_TOKEN, ENV_TOKEN_OLD, @@ -174,6 +175,8 @@ async def run(self, *, restore_job_id: str | None = None) -> None: } if restore_job_id: environment[ENV_RESTORE_JOB_ID] = restore_job_id + if self.sys_homeassistant.duplicate_log_file: + environment[ENV_DUPLICATE_LOG_FILE] = "1" await self._run( tag=(self.sys_homeassistant.version), name=self.name, diff --git a/supervisor/homeassistant/module.py b/supervisor/homeassistant/module.py index 9f2cba6b25c..ce0a895f6b9 100644 --- a/supervisor/homeassistant/module.py +++ b/supervisor/homeassistant/module.py @@ -23,6 +23,7 @@ ATTR_AUDIO_OUTPUT, ATTR_BACKUPS_EXCLUDE_DATABASE, ATTR_BOOT, + ATTR_DUPLICATE_LOG_FILE, ATTR_IMAGE, ATTR_MESSAGE, ATTR_PORT, @@ -299,6 +300,16 @@ def backups_exclude_database(self, value: bool) -> None: """Set whether backups should exclude database by default.""" self._data[ATTR_BACKUPS_EXCLUDE_DATABASE] = value + @property + def duplicate_log_file(self) -> bool: + """Return True if Home Assistant should duplicate logs to file.""" + return self._data[ATTR_DUPLICATE_LOG_FILE] + + @duplicate_log_file.setter + def duplicate_log_file(self, value: bool) -> None: + """Set whether Home Assistant should duplicate logs to file.""" + self._data[ATTR_DUPLICATE_LOG_FILE] = value + async def load(self) -> None: """Prepare Home Assistant object.""" await asyncio.wait( diff --git a/supervisor/homeassistant/validate.py b/supervisor/homeassistant/validate.py index 1c8267d4710..3b9f5eb2f74 100644 --- a/supervisor/homeassistant/validate.py +++ b/supervisor/homeassistant/validate.py @@ -10,6 +10,7 @@ ATTR_AUDIO_OUTPUT, ATTR_BACKUPS_EXCLUDE_DATABASE, ATTR_BOOT, + ATTR_DUPLICATE_LOG_FILE, ATTR_IMAGE, ATTR_PORT, ATTR_REFRESH_TOKEN, @@ -36,6 +37,7 @@ vol.Optional(ATTR_AUDIO_OUTPUT, default=None): vol.Maybe(str), vol.Optional(ATTR_AUDIO_INPUT, default=None): vol.Maybe(str), vol.Optional(ATTR_BACKUPS_EXCLUDE_DATABASE, default=False): vol.Boolean(), + vol.Optional(ATTR_DUPLICATE_LOG_FILE, default=False): vol.Boolean(), vol.Optional(ATTR_OVERRIDE_IMAGE, default=False): vol.Boolean(), }, extra=vol.REMOVE_EXTRA, diff --git a/tests/docker/test_homeassistant.py b/tests/docker/test_homeassistant.py index 5e4833325ad..6df19253f59 100644 --- a/tests/docker/test_homeassistant.py +++ b/tests/docker/test_homeassistant.py @@ -46,6 +46,7 @@ async def test_homeassistant_start( "TZ": ANY, "SUPERVISOR_TOKEN": ANY, "HASSIO_TOKEN": ANY, + # no "HA_DUPLICATE_LOG_FILE" } assert run.call_args.kwargs["mounts"] == [ DEV_MOUNT, @@ -105,6 +106,28 @@ async def test_homeassistant_start( assert "volumes" not in run.call_args.kwargs +async def test_homeassistant_start_with_duplicate_log_file( + coresys: CoreSys, tmp_supervisor_data: Path, path_extern +): + """Test starting homeassistant with duplicate_log_file enabled.""" + coresys.homeassistant.version = AwesomeVersion("2025.12.0") + coresys.homeassistant.duplicate_log_file = True + + with ( + patch.object(DockerAPI, "run") as run, + patch.object( + DockerHomeAssistant, "is_running", side_effect=[False, False, True] + ), + patch("supervisor.homeassistant.core.asyncio.sleep"), + ): + await coresys.homeassistant.core.start() + + run.assert_called_once() + env = run.call_args.kwargs["environment"] + assert "HA_DUPLICATE_LOG_FILE" in env + assert env["HA_DUPLICATE_LOG_FILE"] == "1" + + async def test_landingpage_start( coresys: CoreSys, tmp_supervisor_data: Path, path_extern ): @@ -133,6 +156,7 @@ async def test_landingpage_start( "TZ": ANY, "SUPERVISOR_TOKEN": ANY, "HASSIO_TOKEN": ANY, + # no "HA_DUPLICATE_LOG_FILE" } assert run.call_args.kwargs["mounts"] == [ DEV_MOUNT,