1
0
mirror of https://github.com/home-assistant/core synced 2024-07-05 17:29:15 +00:00

Make type checking pass for deCONZ init, gateway and services (#66054)

* Type and enable type checking for init, config_flow, diagnostics, gateway and services

* Fix import

* Fix review comment
This commit is contained in:
Robert Svensson 2022-02-23 13:10:35 +01:00 committed by GitHub
parent 3afadf8adb
commit dd88a05cb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 180 additions and 150 deletions

View File

@ -58,6 +58,11 @@ homeassistant.components.canary.*
homeassistant.components.cover.*
homeassistant.components.crownstone.*
homeassistant.components.cpuspeed.*
homeassistant.components.deconz
homeassistant.components.deconz.config_flow
homeassistant.components.deconz.diagnostics
homeassistant.components.deconz.gateway
homeassistant.components.deconz.services
homeassistant.components.device_automation.*
homeassistant.components.device_tracker.*
homeassistant.components.devolo_home_control.*

View File

@ -12,11 +12,14 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
import homeassistant.helpers.entity_registry as er
from .config_flow import get_master_gateway
from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN
from .gateway import DeconzGateway
from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN, PLATFORMS
from .deconz_event import async_setup_events, async_unload_events
from .errors import AuthenticationRequired, CannotConnect
from .gateway import DeconzGateway, get_deconz_session
from .services import async_setup_services, async_unload_services
@ -33,16 +36,27 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
if not config_entry.options:
await async_update_master_gateway(hass, config_entry)
gateway = DeconzGateway(hass, config_entry)
if not await gateway.async_setup():
return False
try:
api = await get_deconz_session(hass, config_entry.data)
except CannotConnect as err:
raise ConfigEntryNotReady from err
except AuthenticationRequired as err:
raise ConfigEntryAuthFailed from err
if not hass.data[DOMAIN]:
gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway(
hass, config_entry, api
)
config_entry.add_update_listener(gateway.async_config_entry_updated)
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
await async_setup_events(gateway)
await gateway.async_update_device_registry()
if len(hass.data[DOMAIN]) == 1:
async_setup_services(hass)
hass.data[DOMAIN][config_entry.entry_id] = gateway
await gateway.async_update_device_registry()
api.start()
config_entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown)
@ -53,7 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload deCONZ config entry."""
gateway = hass.data[DOMAIN].pop(config_entry.entry_id)
gateway: DeconzGateway = hass.data[DOMAIN].pop(config_entry.entry_id)
async_unload_events(gateway)
if not hass.data[DOMAIN]:
async_unload_services(hass)
@ -89,9 +104,10 @@ async def async_update_group_unique_id(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Update unique ID entities based on deCONZ groups."""
if not isinstance(old_unique_id := config_entry.data.get(CONF_GROUP_ID_BASE), str):
if not (group_id_base := config_entry.data.get(CONF_GROUP_ID_BASE)):
return
old_unique_id = cast(str, group_id_base)
new_unique_id = cast(str, config_entry.unique_id)
@callback

View File

@ -1,13 +1,21 @@
"""Representation of a deCONZ gateway."""
from __future__ import annotations
import asyncio
from types import MappingProxyType
from typing import Any, cast
import async_timeout
from pydeconz import DeconzSession, errors, group, light, sensor
from pydeconz.alarm_system import AlarmSystem as DeconzAlarmSystem
from pydeconz.group import Group as DeconzGroup
from pydeconz.light import DeconzLight
from pydeconz.sensor import DeconzSensor
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import (
aiohttp_client,
device_registry as dr,
@ -29,25 +37,23 @@ from .const import (
LOGGER,
PLATFORMS,
)
from .deconz_event import async_setup_events, async_unload_events
from .deconz_event import DeconzAlarmEvent, DeconzEvent
from .errors import AuthenticationRequired, CannotConnect
@callback
def get_gateway_from_config_entry(hass, config_entry):
"""Return gateway with a matching config entry ID."""
return hass.data[DECONZ_DOMAIN][config_entry.entry_id]
class DeconzGateway:
"""Manages a single deCONZ gateway."""
def __init__(self, hass, config_entry) -> None:
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: DeconzSession
) -> None:
"""Initialize the system."""
self.hass = hass
self.config_entry = config_entry
self.api = api
self.api = None
api.add_device_callback = self.async_add_device_callback
api.connection_status_callback = self.async_connection_status_callback
self.available = True
self.ignore_state_updates = False
@ -66,24 +72,24 @@ class DeconzGateway:
sensor.RESOURCE_TYPE: self.signal_new_sensor,
}
self.deconz_ids = {}
self.entities = {}
self.events = []
self.deconz_ids: dict[str, str] = {}
self.entities: dict[str, set[str]] = {}
self.events: list[DeconzAlarmEvent | DeconzEvent] = []
@property
def bridgeid(self) -> str:
"""Return the unique identifier of the gateway."""
return 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 self.config_entry.data[CONF_HOST]
return cast(str, self.config_entry.data[CONF_HOST])
@property
def master(self) -> bool:
"""Gateway which is used with deCONZ services without defining id."""
return self.config_entry.options[CONF_MASTER_GATEWAY]
return cast(bool, self.config_entry.options[CONF_MASTER_GATEWAY])
# Options
@ -111,7 +117,7 @@ class DeconzGateway:
# Callbacks
@callback
def async_connection_status_callback(self, available) -> None:
def async_connection_status_callback(self, available: bool) -> None:
"""Handle signals of gateway connection status."""
self.available = available
self.ignore_state_updates = False
@ -119,7 +125,15 @@ class DeconzGateway:
@callback
def async_add_device_callback(
self, resource_type, device=None, force: bool = False
self,
resource_type: str,
device: DeconzAlarmSystem
| DeconzGroup
| DeconzLight
| DeconzSensor
| list[DeconzAlarmSystem | DeconzGroup | DeconzLight | DeconzSensor]
| None = None,
force: bool = False,
) -> None:
"""Handle event of new device creation in deCONZ."""
if (
@ -166,32 +180,6 @@ class DeconzGateway:
via_device=(CONNECTION_NETWORK_MAC, self.api.config.mac),
)
async def async_setup(self) -> bool:
"""Set up a deCONZ gateway."""
try:
self.api = await get_gateway(
self.hass,
self.config_entry.data,
self.async_add_device_callback,
self.async_connection_status_callback,
)
except CannotConnect as err:
raise ConfigEntryNotReady from err
except AuthenticationRequired as err:
raise ConfigEntryAuthFailed from err
self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS)
await async_setup_events(self)
self.api.start()
self.config_entry.add_update_listener(self.async_config_entry_updated)
return True
@staticmethod
async def async_config_entry_updated(
hass: HomeAssistant, entry: ConfigEntry
@ -211,7 +199,7 @@ class DeconzGateway:
await gateway.options_updated()
async def options_updated(self):
async def options_updated(self) -> None:
"""Manage entities affected by config entry options."""
deconz_ids = []
@ -242,14 +230,14 @@ class DeconzGateway:
entity_registry.async_remove(entity_id)
@callback
def shutdown(self, event) -> None:
def shutdown(self, event: Event) -> None:
"""Wrap the call to deconz.close.
Used as an argument to EventBus.async_listen_once.
"""
self.api.close()
async def async_reset(self):
async def async_reset(self) -> bool:
"""Reset this gateway to default state."""
self.api.async_connection_status_callback = None
self.api.close()
@ -258,30 +246,35 @@ class DeconzGateway:
self.config_entry, PLATFORMS
)
async_unload_events(self)
self.deconz_ids = {}
return True
async def get_gateway(
hass, config, async_add_device_callback, async_connection_status_callback
@callback
def get_gateway_from_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> DeconzGateway:
"""Return gateway with a matching config entry ID."""
return cast(DeconzGateway, hass.data[DECONZ_DOMAIN][config_entry.entry_id])
async def get_deconz_session(
hass: HomeAssistant,
config: MappingProxyType[str, Any],
) -> DeconzSession:
"""Create a gateway object and verify configuration."""
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(
deconz_session = DeconzSession(
session,
config[CONF_HOST],
config[CONF_PORT],
config[CONF_API_KEY],
add_device=async_add_device_callback,
connection_status=async_connection_status_callback,
)
try:
async with async_timeout.timeout(10):
await deconz.refresh_state()
return deconz
await deconz_session.refresh_state()
return deconz_session
except errors.Unauthorized as err:
LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST])

View File

@ -1,7 +1,5 @@
"""deCONZ services."""
from types import MappingProxyType
from pydeconz.utils import normalize_bridge_id
import voluptuous as vol
@ -16,6 +14,7 @@ from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry,
async_entries_for_device,
)
from homeassistant.util.read_only_dict import ReadOnlyDict
from .config_flow import get_master_gateway
from .const import CONF_BRIDGE_ID, DOMAIN, LOGGER
@ -111,9 +110,7 @@ def async_unload_services(hass: HomeAssistant) -> None:
hass.services.async_remove(DOMAIN, service)
async def async_configure_service(
gateway: DeconzGateway, data: MappingProxyType
) -> None:
async def async_configure_service(gateway: DeconzGateway, data: ReadOnlyDict) -> None:
"""Set attribute of device in deCONZ.
Entity is used to resolve to a device path (e.g. '/lights/1').

View File

@ -447,6 +447,61 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.deconz]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.deconz.config_flow]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.deconz.diagnostics]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.deconz.gateway]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.deconz.services]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.device_automation.*]
check_untyped_defs = true
disallow_incomplete_defs = true
@ -2209,9 +2264,6 @@ ignore_errors = true
[mypy-homeassistant.components.conversation.default_agent]
ignore_errors = true
[mypy-homeassistant.components.deconz]
ignore_errors = true
[mypy-homeassistant.components.deconz.alarm_control_panel]
ignore_errors = true
@ -2227,9 +2279,6 @@ ignore_errors = true
[mypy-homeassistant.components.deconz.fan]
ignore_errors = true
[mypy-homeassistant.components.deconz.gateway]
ignore_errors = true
[mypy-homeassistant.components.deconz.light]
ignore_errors = true
@ -2245,9 +2294,6 @@ ignore_errors = true
[mypy-homeassistant.components.deconz.sensor]
ignore_errors = true
[mypy-homeassistant.components.deconz.services]
ignore_errors = true
[mypy-homeassistant.components.deconz.siren]
ignore_errors = true

View File

@ -23,19 +23,16 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.cloud.http_api",
"homeassistant.components.conversation",
"homeassistant.components.conversation.default_agent",
"homeassistant.components.deconz",
"homeassistant.components.deconz.alarm_control_panel",
"homeassistant.components.deconz.binary_sensor",
"homeassistant.components.deconz.climate",
"homeassistant.components.deconz.cover",
"homeassistant.components.deconz.fan",
"homeassistant.components.deconz.gateway",
"homeassistant.components.deconz.light",
"homeassistant.components.deconz.lock",
"homeassistant.components.deconz.logbook",
"homeassistant.components.deconz.number",
"homeassistant.components.deconz.sensor",
"homeassistant.components.deconz.services",
"homeassistant.components.deconz.siren",
"homeassistant.components.deconz.switch",
"homeassistant.components.denonavr.config_flow",

View File

@ -1,7 +1,8 @@
"""Test deCONZ gateway."""
import asyncio
from copy import deepcopy
from unittest.mock import Mock, patch
from unittest.mock import patch
import pydeconz
from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING
@ -19,7 +20,7 @@ from homeassistant.components.deconz.config_flow import DECONZ_MANUFACTURERURL
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect
from homeassistant.components.deconz.gateway import (
get_gateway,
get_deconz_session,
get_gateway_from_config_entry,
)
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
@ -202,25 +203,6 @@ async def test_gateway_device_configuration_url_when_addon(hass, aioclient_mock)
)
async def test_gateway_retry(hass):
"""Retry setup."""
with patch(
"homeassistant.components.deconz.gateway.get_gateway",
side_effect=CannotConnect,
):
await setup_deconz_integration(hass)
assert not hass.data[DECONZ_DOMAIN]
async def test_gateway_setup_fails(hass):
"""Retry setup."""
with patch(
"homeassistant.components.deconz.gateway.get_gateway", side_effect=Exception
):
await setup_deconz_integration(hass)
assert not hass.data[DECONZ_DOMAIN]
async def test_connection_status_signalling(
hass, aioclient_mock, mock_deconz_websocket
):
@ -282,18 +264,6 @@ async def test_update_address(hass, aioclient_mock):
assert len(mock_setup_entry.mock_calls) == 1
async def test_gateway_trigger_reauth_flow(hass):
"""Failed authentication trigger a reauthentication flow."""
with patch(
"homeassistant.components.deconz.gateway.get_gateway",
side_effect=AuthenticationRequired,
), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init:
await setup_deconz_integration(hass)
mock_flow_init.assert_called_once()
assert hass.data[DECONZ_DOMAIN] == {}
async def test_reset_after_successful_setup(hass, aioclient_mock):
"""Make sure that connection status triggers a dispatcher send."""
config_entry = await setup_deconz_integration(hass, aioclient_mock)
@ -305,25 +275,24 @@ async def test_reset_after_successful_setup(hass, aioclient_mock):
assert result is True
async def test_get_gateway(hass):
async def test_get_deconz_session(hass):
"""Successful call."""
with patch("pydeconz.DeconzSession.refresh_state", return_value=True):
assert await get_gateway(hass, ENTRY_CONFIG, Mock(), Mock())
assert await get_deconz_session(hass, ENTRY_CONFIG)
async def test_get_gateway_fails_unauthorized(hass):
@pytest.mark.parametrize(
"side_effect, raised_exception",
[
(asyncio.TimeoutError, CannotConnect),
(pydeconz.RequestError, CannotConnect),
(pydeconz.Unauthorized, AuthenticationRequired),
],
)
async def test_get_deconz_session_fails(hass, side_effect, raised_exception):
"""Failed call."""
with patch(
"pydeconz.DeconzSession.refresh_state",
side_effect=pydeconz.errors.Unauthorized,
), pytest.raises(AuthenticationRequired):
assert await get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False
async def test_get_gateway_fails_cannot_connect(hass):
"""Failed call."""
with patch(
"pydeconz.DeconzSession.refresh_state",
side_effect=pydeconz.errors.RequestError,
), pytest.raises(CannotConnect):
assert await get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False
side_effect=side_effect,
), pytest.raises(raised_exception):
assert await get_deconz_session(hass, ENTRY_CONFIG)

View File

@ -1,6 +1,5 @@
"""Test deCONZ component setup process."""
import asyncio
from unittest.mock import patch
from homeassistant.components.deconz import (
@ -13,6 +12,7 @@ from homeassistant.components.deconz.const import (
CONF_GROUP_ID_BASE,
DOMAIN as DECONZ_DOMAIN,
)
from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.helpers import entity_registry as er
@ -42,22 +42,6 @@ async def setup_entry(hass, entry):
assert await async_setup_entry(hass, entry) is True
async def test_setup_entry_fails(hass):
"""Test setup entry fails if deCONZ is not available."""
with patch("pydeconz.DeconzSession.refresh_state", side_effect=Exception):
await setup_deconz_integration(hass)
assert not hass.data[DECONZ_DOMAIN]
async def test_setup_entry_no_available_bridge(hass):
"""Test setup entry fails if deCONZ is not available."""
with patch(
"pydeconz.DeconzSession.refresh_state", side_effect=asyncio.TimeoutError
):
await setup_deconz_integration(hass)
assert not hass.data[DECONZ_DOMAIN]
async def test_setup_entry_successful(hass, aioclient_mock):
"""Test setup entry is successful."""
config_entry = await setup_deconz_integration(hass, aioclient_mock)
@ -67,6 +51,29 @@ async def test_setup_entry_successful(hass, aioclient_mock):
assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master
async def test_setup_entry_fails_config_entry_not_ready(hass):
"""Failed authentication trigger a reauthentication flow."""
with patch(
"homeassistant.components.deconz.get_deconz_session",
side_effect=CannotConnect,
):
await setup_deconz_integration(hass)
assert hass.data[DECONZ_DOMAIN] == {}
async def test_setup_entry_fails_trigger_reauth_flow(hass):
"""Failed authentication trigger a reauthentication flow."""
with patch(
"homeassistant.components.deconz.get_deconz_session",
side_effect=AuthenticationRequired,
), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init:
await setup_deconz_integration(hass)
mock_flow_init.assert_called_once()
assert hass.data[DECONZ_DOMAIN] == {}
async def test_setup_entry_multiple_gateways(hass, aioclient_mock):
"""Test setup entry is successful with multiple gateways."""
config_entry = await setup_deconz_integration(hass, aioclient_mock)