Split out deCONZ config model (#112851)

* Add separate deCONZ config class

* Use config in get_deconz_api
This commit is contained in:
Robert Svensson 2024-03-13 22:49:49 +01:00 committed by GitHub
parent 77917506bb
commit 932e073fee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 67 deletions

View File

@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
await async_update_master_gateway(hass, config_entry) await async_update_master_gateway(hass, config_entry)
try: try:
api = await get_deconz_api(hass, config_entry.data) api = await get_deconz_api(hass, config_entry)
except CannotConnect as err: except CannotConnect as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
except AuthenticationRequired as err: except AuthenticationRequired as err:

View File

@ -1,4 +1,5 @@
"""Internal functionality not part of HA infrastructure.""" """Internal functionality not part of HA infrastructure."""
from .api import get_deconz_api # noqa: F401 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 from .hub import DeconzHub, get_gateway_from_config_entry # noqa: F401

View File

@ -3,37 +3,35 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from types import MappingProxyType
from typing import Any
from pydeconz import DeconzSession, errors 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.core import HomeAssistant
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from ..const import LOGGER from ..const import LOGGER
from ..errors import AuthenticationRequired, CannotConnect from ..errors import AuthenticationRequired, CannotConnect
from .config import DeconzConfig
async def get_deconz_api( async def get_deconz_api(
hass: HomeAssistant, config: MappingProxyType[str, Any] hass: HomeAssistant, config_entry: ConfigEntry
) -> DeconzSession: ) -> DeconzSession:
"""Create a gateway object and verify configuration.""" """Create a gateway object and verify configuration."""
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
api = DeconzSession( config = DeconzConfig.from_config_entry(config_entry)
session, config[CONF_HOST], config[CONF_PORT], config[CONF_API_KEY] api = DeconzSession(session, config.host, config.port, config.api_key)
)
try: try:
async with asyncio.timeout(10): async with asyncio.timeout(10):
await api.refresh_state() await api.refresh_state()
return api return api
except errors.Unauthorized as err: 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 raise AuthenticationRequired from err
except (TimeoutError, errors.RequestError, errors.ResponseError) as 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 raise CannotConnect from err

View File

@ -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,
),
)

View File

@ -12,24 +12,18 @@ from pydeconz.interfaces.groups import GroupHandler
from pydeconz.models.event import EventType from pydeconz.models.event import EventType
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry 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.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import ( from ..const import (
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
CONF_ALLOW_NEW_DEVICES,
CONF_MASTER_GATEWAY, CONF_MASTER_GATEWAY,
DEFAULT_ALLOW_CLIP_SENSOR,
DEFAULT_ALLOW_DECONZ_GROUPS,
DEFAULT_ALLOW_NEW_DEVICES,
DOMAIN as DECONZ_DOMAIN, DOMAIN as DECONZ_DOMAIN,
HASSIO_CONFIGURATION_URL, HASSIO_CONFIGURATION_URL,
PLATFORMS, PLATFORMS,
) )
from .config import DeconzConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from ..deconz_event import ( from ..deconz_event import (
@ -77,6 +71,7 @@ class DeconzHub:
) -> None: ) -> None:
"""Initialize the system.""" """Initialize the system."""
self.hass = hass self.hass = hass
self.config = DeconzConfig.from_config_entry(config_entry)
self.config_entry = config_entry self.config_entry = config_entry
self.api = api self.api = api
@ -99,26 +94,11 @@ class DeconzHub:
self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set() self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set()
self.ignored_devices: 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 @property
def bridgeid(self) -> str: def bridgeid(self) -> str:
"""Return the unique identifier of the gateway.""" """Return the unique identifier of the gateway."""
return cast(str, self.config_entry.unique_id) 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 @property
def master(self) -> bool: def master(self) -> bool:
"""Gateway which is used with deCONZ services without defining id.""" """Gateway which is used with deCONZ services without defining id."""
@ -143,7 +123,7 @@ class DeconzHub:
""" """
if ( if (
not initializing not initializing
and not self.option_allow_new_devices and not self.config.allow_new_devices
and not self.ignore_state_updates and not self.ignore_state_updates
): ):
self.ignored_devices.add((async_add_device, device_id)) self.ignored_devices.add((async_add_device, device_id))
@ -151,14 +131,14 @@ class DeconzHub:
if isinstance(deconz_device_interface, GroupHandler): if isinstance(deconz_device_interface, GroupHandler):
self.deconz_groups.add((async_add_device, device_id)) self.deconz_groups.add((async_add_device, device_id))
if not self.option_allow_deconz_groups: if not self.config.allow_deconz_groups:
return return
if isinstance(deconz_device_interface, SENSORS): if isinstance(deconz_device_interface, SENSORS):
device = deconz_device_interface[device_id] device = deconz_device_interface[device_id]
if device.type.startswith("CLIP") and not always_ignore_clip_sensors: if device.type.startswith("CLIP") and not always_ignore_clip_sensors:
self.clip_sensors.add((async_add_device, device_id)) self.clip_sensors.add((async_add_device, device_id))
if not self.option_allow_clip_sensor: if not self.config.allow_clip_sensor:
return return
add_device_callback(EventType.ADDED, device_id) add_device_callback(EventType.ADDED, device_id)
@ -205,7 +185,7 @@ class DeconzHub:
) )
# Gateway service # 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: if self.config_entry.source == SOURCE_HASSIO:
configuration_url = HASSIO_CONFIGURATION_URL configuration_url = HASSIO_CONFIGURATION_URL
device_registry.async_get_or_create( device_registry.async_get_or_create(
@ -222,7 +202,7 @@ class DeconzHub:
@staticmethod @staticmethod
async def async_config_entry_updated( async def async_config_entry_updated(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> None: ) -> None:
"""Handle signals of config entry being updated. """Handle signals of config entry being updated.
@ -231,32 +211,29 @@ class DeconzHub:
Causes for this is either discovery updating host address or Causes for this is either discovery updating host address or
config entry options changing. 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 # A race condition can occur if multiple config entries are
# unloaded in parallel # unloaded in parallel
return return
gateway = get_gateway_from_config_entry(hass, entry) gateway = get_gateway_from_config_entry(hass, config_entry)
previous_config = gateway.config
if gateway.api.host != gateway.host: gateway.config = DeconzConfig.from_config_entry(config_entry)
if previous_config.host != gateway.config.host:
gateway.api.close() gateway.api.close()
gateway.api.host = gateway.host gateway.api.host = gateway.config.host
gateway.api.start() gateway.api.start()
return 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.""" """Manage entities affected by config entry options."""
deconz_ids = [] deconz_ids = []
# Allow CLIP sensors # Allow CLIP sensors
option_allow_clip_sensor = self.config_entry.options.get( if self.config.allow_clip_sensor != previous_config.allow_clip_sensor:
CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR if self.config.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:
for add_device, device_id in self.clip_sensors: for add_device, device_id in self.clip_sensors:
add_device(EventType.ADDED, device_id) add_device(EventType.ADDED, device_id)
else: else:
@ -268,12 +245,8 @@ class DeconzHub:
# Allow Groups # Allow Groups
option_allow_deconz_groups = self.config_entry.options.get( if self.config.allow_deconz_groups != previous_config.allow_deconz_groups:
CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS if self.config.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:
for add_device, device_id in self.deconz_groups: for add_device, device_id in self.deconz_groups:
add_device(EventType.ADDED, device_id) add_device(EventType.ADDED, device_id)
else: else:
@ -281,12 +254,8 @@ class DeconzHub:
# Allow adding new devices # Allow adding new devices
option_allow_new_devices = self.config_entry.options.get( if self.config.allow_new_devices != previous_config.allow_new_devices:
CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES if self.config.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:
self.load_ignored_devices() self.load_ignored_devices()
# Remove entities based on above categories # Remove entities based on above categories

View File

@ -152,9 +152,9 @@ async def test_gateway_setup(
gateway = get_gateway_from_config_entry(hass, config_entry) gateway = get_gateway_from_config_entry(hass, config_entry)
assert gateway.bridgeid == BRIDGEID assert gateway.bridgeid == BRIDGEID
assert gateway.master is True assert gateway.master is True
assert gateway.option_allow_clip_sensor is False assert gateway.config.allow_clip_sensor is False
assert gateway.option_allow_deconz_groups is True assert gateway.config.allow_deconz_groups is True
assert gateway.option_allow_new_devices is True assert gateway.config.allow_new_devices is True
assert len(gateway.deconz_ids) == 0 assert len(gateway.deconz_ids) == 0
assert len(hass.states.async_all()) == 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: async def test_get_deconz_api(hass: HomeAssistant) -> None:
"""Successful call.""" """Successful call."""
config_entry = MockConfigEntry(domain=DECONZ_DOMAIN, data=ENTRY_CONFIG)
with patch("pydeconz.DeconzSession.refresh_state", return_value=True): 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( @pytest.mark.parametrize(
@ -307,8 +308,9 @@ async def test_get_deconz_api_fails(
hass: HomeAssistant, side_effect, raised_exception hass: HomeAssistant, side_effect, raised_exception
) -> None: ) -> None:
"""Failed call.""" """Failed call."""
config_entry = MockConfigEntry(domain=DECONZ_DOMAIN, data=ENTRY_CONFIG)
with patch( with patch(
"pydeconz.DeconzSession.refresh_state", "pydeconz.DeconzSession.refresh_state",
side_effect=side_effect, side_effect=side_effect,
), pytest.raises(raised_exception): ), pytest.raises(raised_exception):
assert await get_deconz_api(hass, ENTRY_CONFIG) assert await get_deconz_api(hass, config_entry)