diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 23b65d0ff5f..c5c6be6094d 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from copy import deepcopy import ipaddress import logging import os @@ -348,8 +349,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: @callback def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): - options = dict(entry.options) - data = dict(entry.data) + options = deepcopy(dict(entry.options)) + data = deepcopy(dict(entry.data)) modified = False for importable_option in CONFIG_OPTIONS: if importable_option not in entry.options and importable_option in entry.data: diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 445b20ac548..ac932b1c31b 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -2,9 +2,11 @@ from __future__ import annotations import asyncio +from copy import deepcopy import random import re import string +from typing import Final import voluptuous as vol @@ -117,7 +119,7 @@ DEFAULT_DOMAINS = [ "water_heater", ] -_EMPTY_ENTITY_FILTER = { +_EMPTY_ENTITY_FILTER: Final = { CONF_INCLUDE_DOMAINS: [], CONF_EXCLUDE_DOMAINS: [], CONF_INCLUDE_ENTITIES: [], @@ -152,7 +154,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Choose specific domains in bridge mode.""" if user_input is not None: - entity_filter = _EMPTY_ENTITY_FILTER.copy() + entity_filter = deepcopy(_EMPTY_ENTITY_FILTER) entity_filter[CONF_INCLUDE_DOMAINS] = user_input[CONF_INCLUDE_DOMAINS] self.hk_data[CONF_FILTER] = entity_filter return await self.async_step_pairing() @@ -493,7 +495,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.hk_options.update(user_input) return await self.async_step_include_exclude() - self.hk_options = dict(self.config_entry.options) + self.hk_options = deepcopy(dict(self.config_entry.options)) entity_filter = self.hk_options.get(CONF_FILTER, {}) homekit_mode = self.hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) domains = entity_filter.get(CONF_INCLUDE_DOMAINS, []) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index d190dec04b8..ffd223d1d2a 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -2,9 +2,14 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow -from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME +from homeassistant.components.homekit.const import ( + CONF_FILTER, + DOMAIN, + SHORT_BRIDGE_NAME, +) from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS from homeassistant.setup import async_setup_component from .util import PATH_HOMEKIT, async_init_entry @@ -347,6 +352,10 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "include_exclude" + # Inject garbage to ensure the options data + # is being deep copied and we cannot mutate it in flight + config_entry.options[CONF_FILTER][CONF_INCLUDE_DOMAINS].append("garbage") + result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.old"], "include_exclude_mode": "exclude"},