Add typing to deCONZ init and config flow (#59999)

pull/60778/head
Robert Svensson 2021-12-01 18:59:52 +01:00 committed by GitHub
parent a053c0a106
commit 8ddfa424c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 44 deletions

View File

@ -1,12 +1,18 @@
"""Support for deCONZ devices.""" """Support for deCONZ devices."""
from __future__ import annotations
from typing import cast
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_HOST, CONF_HOST,
CONF_PORT, CONF_PORT,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_registry import async_migrate_entries import homeassistant.helpers.entity_registry as er
from .config_flow import get_master_gateway from .config_flow import get_master_gateway
from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN
@ -14,7 +20,7 @@ from .gateway import DeconzGateway
from .services import async_setup_services, async_unload_services from .services import async_setup_services, async_unload_services
async def async_setup_entry(hass, config_entry): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up a deCONZ bridge for a config entry. """Set up a deCONZ bridge for a config entry.
Load config, group, light and sensor data for server information. Load config, group, light and sensor data for server information.
@ -28,7 +34,6 @@ async def async_setup_entry(hass, config_entry):
await async_update_master_gateway(hass, config_entry) await async_update_master_gateway(hass, config_entry)
gateway = DeconzGateway(hass, config_entry) gateway = DeconzGateway(hass, config_entry)
if not await gateway.async_setup(): if not await gateway.async_setup():
return False return False
@ -46,7 +51,7 @@ async def async_setup_entry(hass, config_entry):
return True return True
async def async_unload_entry(hass, config_entry): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload deCONZ config entry.""" """Unload deCONZ config entry."""
gateway = hass.data[DOMAIN].pop(config_entry.entry_id) gateway = hass.data[DOMAIN].pop(config_entry.entry_id)
@ -61,27 +66,36 @@ async def async_unload_entry(hass, config_entry):
return await gateway.async_reset() return await gateway.async_reset()
async def async_update_master_gateway(hass, config_entry): async def async_update_master_gateway(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Update master gateway boolean. """Update master gateway boolean.
Called by setup_entry and unload_entry. Called by setup_entry and unload_entry.
Makes sure there is always one master available. Makes sure there is always one master available.
""" """
master = not get_master_gateway(hass) try:
master_gateway = get_master_gateway(hass)
master = master_gateway.config_entry == config_entry
except ValueError:
master = True
options = {**config_entry.options, CONF_MASTER_GATEWAY: master} options = {**config_entry.options, CONF_MASTER_GATEWAY: master}
hass.config_entries.async_update_entry(config_entry, options=options) hass.config_entries.async_update_entry(config_entry, options=options)
async def async_update_group_unique_id(hass, config_entry) -> None: async def async_update_group_unique_id(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Update unique ID entities based on deCONZ groups.""" """Update unique ID entities based on deCONZ groups."""
if not (old_unique_id := config_entry.data.get(CONF_GROUP_ID_BASE)): if not isinstance(old_unique_id := config_entry.data.get(CONF_GROUP_ID_BASE), str):
return return
new_unique_id: str = config_entry.unique_id new_unique_id = cast(str, config_entry.unique_id)
@callback @callback
def update_unique_id(entity_entry): def update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None:
"""Update unique ID of entity entry.""" """Update unique ID of entity entry."""
if f"{old_unique_id}-" not in entity_entry.unique_id: if f"{old_unique_id}-" not in entity_entry.unique_id:
return None return None
@ -91,7 +105,7 @@ async def async_update_group_unique_id(hass, config_entry) -> None:
) )
} }
await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
data = { data = {
CONF_API_KEY: config_entry.data[CONF_API_KEY], CONF_API_KEY: config_entry.data[CONF_API_KEY],
CONF_HOST: config_entry.data[CONF_HOST], CONF_HOST: config_entry.data[CONF_HOST],

View File

@ -1,6 +1,10 @@
"""Config flow to configure deCONZ component.""" """Config flow to configure deCONZ component."""
from __future__ import annotations
import asyncio import asyncio
from pprint import pformat from pprint import pformat
from typing import Any, cast
from urllib.parse import urlparse from urllib.parse import urlparse
import async_timeout import async_timeout
@ -15,8 +19,11 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import ssdp from homeassistant.components import ssdp
from homeassistant.components.deconz.gateway import DeconzGateway
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import ( from .const import (
@ -36,33 +43,36 @@ CONF_MANUAL_INPUT = "Manually define gateway"
@callback @callback
def get_master_gateway(hass): def get_master_gateway(hass: HomeAssistant) -> DeconzGateway:
"""Return the gateway which is marked as master.""" """Return the gateway which is marked as master."""
for gateway in hass.data[DOMAIN].values(): for gateway in hass.data[DOMAIN].values():
if gateway.master: if gateway.master:
return gateway return cast(DeconzGateway, gateway)
raise ValueError
class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a deCONZ config flow.""" """Handle a deCONZ config flow."""
VERSION = 1 VERSION = 1
_hassio_discovery = None _hassio_discovery: dict[str, Any]
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return DeconzOptionsFlowHandler(config_entry) return DeconzOptionsFlowHandler(config_entry)
def __init__(self): def __init__(self) -> None:
"""Initialize the deCONZ config flow.""" """Initialize the deCONZ config flow."""
self.bridge_id = None self.bridge_id = ""
self.bridges = [] self.bridges: list[dict[str, int | str]] = []
self.deconz_config = {} self.deconz_config: dict[str, int | str] = {}
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a deCONZ config flow start. """Handle a deCONZ config flow start.
Let user choose between discovered bridges and manual configuration. Let user choose between discovered bridges and manual configuration.
@ -75,7 +85,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
for bridge in self.bridges: for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]: if bridge[CONF_HOST] == user_input[CONF_HOST]:
self.bridge_id = bridge[CONF_BRIDGE_ID] self.bridge_id = cast(str, bridge[CONF_BRIDGE_ID])
self.deconz_config = { self.deconz_config = {
CONF_HOST: bridge[CONF_HOST], CONF_HOST: bridge[CONF_HOST],
CONF_PORT: bridge[CONF_PORT], CONF_PORT: bridge[CONF_PORT],
@ -108,7 +118,9 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_manual_input() return await self.async_step_manual_input()
async def async_step_manual_input(self, user_input=None): async def async_step_manual_input(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manual configuration.""" """Manual configuration."""
if user_input: if user_input:
self.deconz_config = user_input self.deconz_config = user_input
@ -124,9 +136,11 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
), ),
) )
async def async_step_link(self, user_input=None): async def async_step_link(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Attempt to link with the deCONZ bridge.""" """Attempt to link with the deCONZ bridge."""
errors = {} errors: dict[str, str] = {}
LOGGER.debug( LOGGER.debug(
"Preparing linking with deCONZ gateway %s", pformat(self.deconz_config) "Preparing linking with deCONZ gateway %s", pformat(self.deconz_config)
@ -153,7 +167,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="link", errors=errors) return self.async_show_form(step_id="link", errors=errors)
async def _create_entry(self): async def _create_entry(self) -> FlowResult:
"""Create entry for gateway.""" """Create entry for gateway."""
if not self.bridge_id: if not self.bridge_id:
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
@ -178,7 +192,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) return self.async_create_entry(title=self.bridge_id, data=self.deconz_config)
async def async_step_reauth(self, config: dict): async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult:
"""Trigger a reauthentication flow.""" """Trigger a reauthentication flow."""
self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]} self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]}
@ -189,7 +203,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_link() return await self.async_step_link()
async def async_step_ssdp(self, discovery_info): async def async_step_ssdp(self, discovery_info: dict[str, str]) -> FlowResult:
"""Handle a discovered deCONZ bridge.""" """Handle a discovered deCONZ bridge."""
if ( if (
discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL)
@ -206,20 +220,20 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if entry and entry.source == config_entries.SOURCE_HASSIO: if entry and entry.source == config_entries.SOURCE_HASSIO:
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
hostname = cast(str, parsed_url.hostname)
port = cast(int, parsed_url.port)
self._abort_if_unique_id_configured( self._abort_if_unique_id_configured(
updates={CONF_HOST: parsed_url.hostname, CONF_PORT: parsed_url.port} updates={CONF_HOST: hostname, CONF_PORT: port}
) )
self.context["title_placeholders"] = {"host": parsed_url.hostname} self.context["title_placeholders"] = {"host": hostname}
self.deconz_config = { self.deconz_config = {CONF_HOST: hostname, CONF_PORT: port}
CONF_HOST: parsed_url.hostname,
CONF_PORT: parsed_url.port,
}
return await self.async_step_link() return await self.async_step_link()
async def async_step_hassio(self, discovery_info): async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult:
"""Prepare configuration for a Hass.io deCONZ bridge. """Prepare configuration for a Hass.io deCONZ bridge.
This flow is triggered by the discovery component. This flow is triggered by the discovery component.
@ -241,8 +255,11 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_hassio_confirm() return await self.async_step_hassio_confirm()
async def async_step_hassio_confirm(self, user_input=None): async def async_step_hassio_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm a Hass.io discovery.""" """Confirm a Hass.io discovery."""
if user_input is not None: if user_input is not None:
self.deconz_config = { self.deconz_config = {
CONF_HOST: self._hassio_discovery[CONF_HOST], CONF_HOST: self._hassio_discovery[CONF_HOST],
@ -258,21 +275,26 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) )
class DeconzOptionsFlowHandler(config_entries.OptionsFlow): class DeconzOptionsFlowHandler(OptionsFlow):
"""Handle deCONZ options.""" """Handle deCONZ options."""
def __init__(self, config_entry): gateway: DeconzGateway
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize deCONZ options flow.""" """Initialize deCONZ options flow."""
self.config_entry = config_entry self.config_entry = config_entry
self.options = dict(config_entry.options) self.options = dict(config_entry.options)
self.gateway = None
async def async_step_init(self, user_input=None): async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the deCONZ options.""" """Manage the deCONZ options."""
self.gateway = get_gateway_from_config_entry(self.hass, self.config_entry) self.gateway = get_gateway_from_config_entry(self.hass, self.config_entry)
return await self.async_step_deconz_devices() return await self.async_step_deconz_devices()
async def async_step_deconz_devices(self, user_input=None): async def async_step_deconz_devices(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the deconz devices options.""" """Manage the deconz devices options."""
if user_input is not None: if user_input is not None:
self.options.update(user_input) self.options.update(user_input)

View File

@ -66,7 +66,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
service = service_call.service service = service_call.service
service_data = service_call.data service_data = service_call.data
gateway = get_master_gateway(hass)
if CONF_BRIDGE_ID in service_data: if CONF_BRIDGE_ID in service_data:
found_gateway = False found_gateway = False
bridge_id = normalize_bridge_id(service_data[CONF_BRIDGE_ID]) bridge_id = normalize_bridge_id(service_data[CONF_BRIDGE_ID])
@ -80,6 +79,12 @@ def async_setup_services(hass: HomeAssistant) -> None:
if not found_gateway: if not found_gateway:
LOGGER.error("Could not find the gateway %s", bridge_id) LOGGER.error("Could not find the gateway %s", bridge_id)
return return
else:
try:
gateway = get_master_gateway(hass)
except ValueError:
LOGGER.error("No master gateway available")
return
if service == SERVICE_CONFIGURE_DEVICE: if service == SERVICE_CONFIGURE_DEVICE:
await async_configure_service(gateway, service_data) await async_configure_service(gateway, service_data)

View File

@ -6,6 +6,7 @@ import voluptuous as vol
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
CONF_BRIDGE_ID, CONF_BRIDGE_ID,
CONF_MASTER_GATEWAY,
DOMAIN as DECONZ_DOMAIN, DOMAIN as DECONZ_DOMAIN,
) )
from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT
@ -187,6 +188,26 @@ async def test_configure_service_with_faulty_entity(hass, aioclient_mock):
assert len(aioclient_mock.mock_calls) == 0 assert len(aioclient_mock.mock_calls) == 0
async def test_calling_service_with_no_master_gateway_fails(hass, aioclient_mock):
"""Test that service call fails when no master gateway exist."""
await setup_deconz_integration(
hass, aioclient_mock, options={CONF_MASTER_GATEWAY: False}
)
aioclient_mock.clear_requests()
data = {
SERVICE_FIELD: "/lights/1",
SERVICE_DATA: {"on": True},
}
await hass.services.async_call(
DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data
)
await hass.async_block_till_done()
assert len(aioclient_mock.mock_calls) == 0
async def test_service_refresh_devices(hass, aioclient_mock): async def test_service_refresh_devices(hass, aioclient_mock):
"""Test that service can refresh devices.""" """Test that service can refresh devices."""
config_entry = await setup_deconz_integration(hass, aioclient_mock) config_entry = await setup_deconz_integration(hass, aioclient_mock)