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)
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:

View File

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

View File

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

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 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

View File

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