Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reauth flow to dormakaba dkey #90225

Merged
merged 2 commits into from Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions homeassistant/components/dormakaba_dkey/__init__.py
Expand Up @@ -5,15 +5,15 @@
import logging

from py_dormakaba_dkey import DKEYLock
from py_dormakaba_dkey.errors import DKEY_EXCEPTIONS
from py_dormakaba_dkey.errors import DKEY_EXCEPTIONS, NotAssociated
from py_dormakaba_dkey.models import AssociationData

from homeassistant.components import bluetooth
from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import CONF_ASSOCIATION_DATA, DOMAIN, UPDATE_SECONDS
Expand Down Expand Up @@ -60,6 +60,8 @@ async def _async_update() -> None:
try:
await lock.update()
await lock.disconnect()
except NotAssociated as ex:
raise ConfigEntryAuthFailed("Not associated") from ex
except DKEY_EXCEPTIONS as ex:
raise UpdateFailed(str(ex)) from ex

Expand Down
50 changes: 45 additions & 5 deletions homeassistant/components/dormakaba_dkey/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for Dormakaba dKey integration."""
from __future__ import annotations

from collections.abc import Mapping
import logging
from typing import Any

Expand All @@ -12,6 +13,7 @@
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
async_last_service_info,
)
from homeassistant.const import CONF_ADDRESS
from homeassistant.data_entry_flow import FlowResult
Expand All @@ -32,12 +34,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):

VERSION = 1

_reauth_entry: config_entries.ConfigEntry | None = None

def __init__(self) -> None:
"""Initialize the config flow."""
self._lock: DKEYLock | None = None
# Populated by user step
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
# Populated by bluetooth and user steps
# Populated by bluetooth, reauth_confirm and user steps
self._discovery_info: BluetoothServiceInfoBleak | None = None

async def async_step_user(
Expand Down Expand Up @@ -113,6 +117,36 @@ async def async_step_bluetooth_confirm(

return await self.async_step_associate()

async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle reauthorization request."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle reauthorization flow."""
errors = {}
reauth_entry = self._reauth_entry
assert reauth_entry is not None

if user_input is not None:
if (
discovery_info := async_last_service_info(
self.hass, reauth_entry.data[CONF_ADDRESS], True
)
) is None:
errors = {"base": "no_longer_in_range"}
else:
self._discovery_info = discovery_info
return await self.async_step_associate()

return self.async_show_form(
step_id="reauth_confirm", data_schema=vol.Schema({}), errors=errors
)

async def async_step_associate(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
Expand Down Expand Up @@ -143,14 +177,20 @@ async def async_step_associate(
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
else:
data = {
CONF_ADDRESS: self._discovery_info.device.address,
CONF_ASSOCIATION_DATA: association_data.to_json(),
}
if reauth_entry := self._reauth_entry:
self.hass.config_entries.async_update_entry(reauth_entry, data=data)
await self.hass.config_entries.async_reload(reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")

return self.async_create_entry(
title=lock.device_info.device_name
or lock.device_info.device_id
or lock.name,
data={
CONF_ADDRESS: self._discovery_info.device.address,
CONF_ASSOCIATION_DATA: association_data.to_json(),
},
data=data,
)

return self.async_show_form(
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/dormakaba_dkey/strings.json
bdraco marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -11,6 +11,9 @@
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
},
"reauth_confirm": {
"description": "The activation code is no longer valid, a new unused activation code is needed.\n\n"
},
"associate": {
"description": "Provide an unused activation code.\n\nTo create an activation code, create a new key in the dKey admin app, then choose to share the key and share an activation code.\n\nMake sure to close the dKey admin app before proceeding.",
"data": {
Expand All @@ -19,13 +22,15 @@
}
},
"error": {
"no_longer_in_range": "The lock is no longer in Bluetooth range. Move the lock or adapter and try again.",
"invalid_code": "Invalid activation code. An activation code consist of 8 characters, separated by a dash, e.g. GBZT-HXC0.",
"wrong_code": "Wrong activation code. Note that an activation code can only be used once."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
Expand Down
63 changes: 63 additions & 0 deletions tests/components/dormakaba_dkey/test_config_flow.py
Expand Up @@ -296,3 +296,66 @@ async def test_bluetooth_step_cannot_associate(hass: HomeAssistant, exc, error)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "associate"
assert result["errors"] == {"base": error}


async def test_reauth(hass: HomeAssistant) -> None:
"""Test reauthentication."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=DKEY_DISCOVERY_INFO.address,
data={"address": DKEY_DISCOVERY_INFO.address},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id},
data=entry.data,
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"

with patch(
"homeassistant.components.dormakaba_dkey.config_flow.async_last_service_info",
return_value=None,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
await hass.async_block_till_done()

assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {"base": "no_longer_in_range"}

with patch(
"homeassistant.components.dormakaba_dkey.config_flow.async_last_service_info",
return_value=DKEY_DISCOVERY_INFO,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
await hass.async_block_till_done()

assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "associate"
assert result["errors"] is None

with patch(
"homeassistant.components.dormakaba_dkey.config_flow.DKEYLock.associate",
return_value=AssociationData(b"1234", b"AABBCCDD"),
) as mock_associate, patch(
"homeassistant.components.dormakaba_dkey.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"activation_code": "1234-1234"}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert entry.data == {
CONF_ADDRESS: DKEY_DISCOVERY_INFO.address,
"association_data": {"key_holder_id": "31323334", "secret": "4141424243434444"},
}
mock_associate.assert_awaited_once_with("1234-1234")