diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 140fbe12d82..776ab8e830f 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await async_update_master_gateway(hass, config_entry) try: - api = await get_deconz_api(hass, config_entry.data) + api = await get_deconz_api(hass, config_entry) except CannotConnect as err: raise ConfigEntryNotReady from err except AuthenticationRequired as err: diff --git a/homeassistant/components/deconz/hub/__init__.py b/homeassistant/components/deconz/hub/__init__.py index a99a578a89d..b3b70ef2207 100644 --- a/homeassistant/components/deconz/hub/__init__.py +++ b/homeassistant/components/deconz/hub/__init__.py @@ -1,4 +1,5 @@ """Internal functionality not part of HA infrastructure.""" from .api import get_deconz_api # noqa: F401 +from .config import DeconzConfig # noqa: F401 from .hub import DeconzHub, get_gateway_from_config_entry # noqa: F401 diff --git a/homeassistant/components/deconz/hub/api.py b/homeassistant/components/deconz/hub/api.py index 33c49e189d5..71551ead6e1 100644 --- a/homeassistant/components/deconz/hub/api.py +++ b/homeassistant/components/deconz/hub/api.py @@ -3,37 +3,35 @@ from __future__ import annotations import asyncio -from types import MappingProxyType -from typing import Any from pydeconz import DeconzSession, errors -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from ..const import LOGGER from ..errors import AuthenticationRequired, CannotConnect +from .config import DeconzConfig async def get_deconz_api( - hass: HomeAssistant, config: MappingProxyType[str, Any] + hass: HomeAssistant, config_entry: ConfigEntry ) -> DeconzSession: """Create a gateway object and verify configuration.""" session = aiohttp_client.async_get_clientsession(hass) - api = DeconzSession( - session, config[CONF_HOST], config[CONF_PORT], config[CONF_API_KEY] - ) + config = DeconzConfig.from_config_entry(config_entry) + api = DeconzSession(session, config.host, config.port, config.api_key) try: async with asyncio.timeout(10): await api.refresh_state() return api except errors.Unauthorized as err: - LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) + LOGGER.warning("Invalid key for deCONZ at %s", config.host) raise AuthenticationRequired from err except (TimeoutError, errors.RequestError, errors.ResponseError) as err: - LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) + LOGGER.error("Error connecting to deCONZ gateway at %s", config.host) raise CannotConnect from err diff --git a/homeassistant/components/deconz/hub/config.py b/homeassistant/components/deconz/hub/config.py new file mode 100644 index 00000000000..06d2dc10542 --- /dev/null +++ b/homeassistant/components/deconz/hub/config.py @@ -0,0 +1,57 @@ +"""deCONZ config entry abstraction.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Self + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT + +from ..const import ( + CONF_ALLOW_CLIP_SENSOR, + CONF_ALLOW_DECONZ_GROUPS, + CONF_ALLOW_NEW_DEVICES, + DEFAULT_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_DECONZ_GROUPS, + DEFAULT_ALLOW_NEW_DEVICES, +) + + +@dataclass +class DeconzConfig: + """Represent a deCONZ config entry.""" + + entry: ConfigEntry + + host: str + port: int + api_key: str + + allow_clip_sensor: bool + allow_deconz_groups: bool + allow_new_devices: bool + + @classmethod + def from_config_entry(cls, config_entry: ConfigEntry) -> Self: + """Create object from config entry.""" + config = config_entry.data + options = config_entry.options + return cls( + entry=config_entry, + host=config[CONF_HOST], + port=config[CONF_PORT], + api_key=config[CONF_API_KEY], + allow_clip_sensor=options.get( + CONF_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_CLIP_SENSOR, + ), + allow_deconz_groups=options.get( + CONF_ALLOW_DECONZ_GROUPS, + DEFAULT_ALLOW_DECONZ_GROUPS, + ), + allow_new_devices=options.get( + CONF_ALLOW_NEW_DEVICES, + DEFAULT_ALLOW_NEW_DEVICES, + ), + ) diff --git a/homeassistant/components/deconz/hub/hub.py b/homeassistant/components/deconz/hub/hub.py index 5c422f845bd..c79ae90aa1a 100644 --- a/homeassistant/components/deconz/hub/hub.py +++ b/homeassistant/components/deconz/hub/hub.py @@ -12,24 +12,18 @@ from pydeconz.interfaces.groups import GroupHandler from pydeconz.models.event import EventType from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( - CONF_ALLOW_CLIP_SENSOR, - CONF_ALLOW_DECONZ_GROUPS, - CONF_ALLOW_NEW_DEVICES, CONF_MASTER_GATEWAY, - DEFAULT_ALLOW_CLIP_SENSOR, - DEFAULT_ALLOW_DECONZ_GROUPS, - DEFAULT_ALLOW_NEW_DEVICES, DOMAIN as DECONZ_DOMAIN, HASSIO_CONFIGURATION_URL, PLATFORMS, ) +from .config import DeconzConfig if TYPE_CHECKING: from ..deconz_event import ( @@ -77,6 +71,7 @@ class DeconzHub: ) -> None: """Initialize the system.""" self.hass = hass + self.config = DeconzConfig.from_config_entry(config_entry) self.config_entry = config_entry self.api = api @@ -99,26 +94,11 @@ class DeconzHub: self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set() self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() - self.option_allow_clip_sensor = self.config_entry.options.get( - CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR - ) - self.option_allow_deconz_groups = config_entry.options.get( - CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS - ) - self.option_allow_new_devices = config_entry.options.get( - CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES - ) - @property def bridgeid(self) -> str: """Return the unique identifier of the gateway.""" return cast(str, self.config_entry.unique_id) - @property - def host(self) -> str: - """Return the host of the gateway.""" - return cast(str, self.config_entry.data[CONF_HOST]) - @property def master(self) -> bool: """Gateway which is used with deCONZ services without defining id.""" @@ -143,7 +123,7 @@ class DeconzHub: """ if ( not initializing - and not self.option_allow_new_devices + and not self.config.allow_new_devices and not self.ignore_state_updates ): self.ignored_devices.add((async_add_device, device_id)) @@ -151,14 +131,14 @@ class DeconzHub: if isinstance(deconz_device_interface, GroupHandler): self.deconz_groups.add((async_add_device, device_id)) - if not self.option_allow_deconz_groups: + if not self.config.allow_deconz_groups: return if isinstance(deconz_device_interface, SENSORS): device = deconz_device_interface[device_id] if device.type.startswith("CLIP") and not always_ignore_clip_sensors: self.clip_sensors.add((async_add_device, device_id)) - if not self.option_allow_clip_sensor: + if not self.config.allow_clip_sensor: return add_device_callback(EventType.ADDED, device_id) @@ -205,7 +185,7 @@ class DeconzHub: ) # Gateway service - configuration_url = f"http://{self.host}:{self.config_entry.data[CONF_PORT]}" + configuration_url = f"http://{self.config.host}:{self.config.port}" if self.config_entry.source == SOURCE_HASSIO: configuration_url = HASSIO_CONFIGURATION_URL device_registry.async_get_or_create( @@ -222,7 +202,7 @@ class DeconzHub: @staticmethod async def async_config_entry_updated( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Handle signals of config entry being updated. @@ -231,32 +211,29 @@ class DeconzHub: Causes for this is either discovery updating host address or config entry options changing. """ - if entry.entry_id not in hass.data[DECONZ_DOMAIN]: + if config_entry.entry_id not in hass.data[DECONZ_DOMAIN]: # A race condition can occur if multiple config entries are # unloaded in parallel return - gateway = get_gateway_from_config_entry(hass, entry) - - if gateway.api.host != gateway.host: + gateway = get_gateway_from_config_entry(hass, config_entry) + previous_config = gateway.config + gateway.config = DeconzConfig.from_config_entry(config_entry) + if previous_config.host != gateway.config.host: gateway.api.close() - gateway.api.host = gateway.host + gateway.api.host = gateway.config.host gateway.api.start() return - await gateway.options_updated() + await gateway.options_updated(previous_config) - async def options_updated(self) -> None: + async def options_updated(self, previous_config: DeconzConfig) -> None: """Manage entities affected by config entry options.""" deconz_ids = [] # Allow CLIP sensors - option_allow_clip_sensor = self.config_entry.options.get( - CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR - ) - if option_allow_clip_sensor != self.option_allow_clip_sensor: - self.option_allow_clip_sensor = option_allow_clip_sensor - if option_allow_clip_sensor: + if self.config.allow_clip_sensor != previous_config.allow_clip_sensor: + if self.config.allow_clip_sensor: for add_device, device_id in self.clip_sensors: add_device(EventType.ADDED, device_id) else: @@ -268,12 +245,8 @@ class DeconzHub: # Allow Groups - option_allow_deconz_groups = self.config_entry.options.get( - CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS - ) - if option_allow_deconz_groups != self.option_allow_deconz_groups: - self.option_allow_deconz_groups = option_allow_deconz_groups - if option_allow_deconz_groups: + if self.config.allow_deconz_groups != previous_config.allow_deconz_groups: + if self.config.allow_deconz_groups: for add_device, device_id in self.deconz_groups: add_device(EventType.ADDED, device_id) else: @@ -281,12 +254,8 @@ class DeconzHub: # Allow adding new devices - option_allow_new_devices = self.config_entry.options.get( - CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES - ) - if option_allow_new_devices != self.option_allow_new_devices: - self.option_allow_new_devices = option_allow_new_devices - if option_allow_new_devices: + if self.config.allow_new_devices != previous_config.allow_new_devices: + if self.config.allow_new_devices: self.load_ignored_devices() # Remove entities based on above categories diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index aec39dc5b8b..a1b28ada799 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -152,9 +152,9 @@ async def test_gateway_setup( gateway = get_gateway_from_config_entry(hass, config_entry) assert gateway.bridgeid == BRIDGEID assert gateway.master is True - assert gateway.option_allow_clip_sensor is False - assert gateway.option_allow_deconz_groups is True - assert gateway.option_allow_new_devices is True + assert gateway.config.allow_clip_sensor is False + assert gateway.config.allow_deconz_groups is True + assert gateway.config.allow_new_devices is True assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 @@ -290,8 +290,9 @@ async def test_reset_after_successful_setup( async def test_get_deconz_api(hass: HomeAssistant) -> None: """Successful call.""" + config_entry = MockConfigEntry(domain=DECONZ_DOMAIN, data=ENTRY_CONFIG) with patch("pydeconz.DeconzSession.refresh_state", return_value=True): - assert await get_deconz_api(hass, ENTRY_CONFIG) + assert await get_deconz_api(hass, config_entry) @pytest.mark.parametrize( @@ -307,8 +308,9 @@ async def test_get_deconz_api_fails( hass: HomeAssistant, side_effect, raised_exception ) -> None: """Failed call.""" + config_entry = MockConfigEntry(domain=DECONZ_DOMAIN, data=ENTRY_CONFIG) with patch( "pydeconz.DeconzSession.refresh_state", side_effect=side_effect, ), pytest.raises(raised_exception): - assert await get_deconz_api(hass, ENTRY_CONFIG) + assert await get_deconz_api(hass, config_entry)