mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Store tplink credentials_hash outside of device_config (#120597)
This commit is contained in:
parent
0d53ce4fb8
commit
970dd99226
@ -43,6 +43,7 @@ from homeassistant.helpers.event import async_track_time_interval
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_CREDENTIALS_HASH,
|
||||||
CONF_DEVICE_CONFIG,
|
CONF_DEVICE_CONFIG,
|
||||||
CONNECT_TIMEOUT,
|
CONNECT_TIMEOUT,
|
||||||
DISCOVERY_TIMEOUT,
|
DISCOVERY_TIMEOUT,
|
||||||
@ -73,6 +74,7 @@ def async_trigger_discovery(
|
|||||||
discovered_devices: dict[str, Device],
|
discovered_devices: dict[str, Device],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Trigger config flows for discovered devices."""
|
"""Trigger config flows for discovered devices."""
|
||||||
|
|
||||||
for formatted_mac, device in discovered_devices.items():
|
for formatted_mac, device in discovered_devices.items():
|
||||||
discovery_flow.async_create_flow(
|
discovery_flow.async_create_flow(
|
||||||
hass,
|
hass,
|
||||||
@ -83,7 +85,6 @@ def async_trigger_discovery(
|
|||||||
CONF_HOST: device.host,
|
CONF_HOST: device.host,
|
||||||
CONF_MAC: formatted_mac,
|
CONF_MAC: formatted_mac,
|
||||||
CONF_DEVICE_CONFIG: device.config.to_dict(
|
CONF_DEVICE_CONFIG: device.config.to_dict(
|
||||||
credentials_hash=device.credentials_hash,
|
|
||||||
exclude_credentials=True,
|
exclude_credentials=True,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -133,6 +134,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
|||||||
"""Set up TPLink from a config entry."""
|
"""Set up TPLink from a config entry."""
|
||||||
host: str = entry.data[CONF_HOST]
|
host: str = entry.data[CONF_HOST]
|
||||||
credentials = await get_credentials(hass)
|
credentials = await get_credentials(hass)
|
||||||
|
entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH)
|
||||||
|
|
||||||
config: DeviceConfig | None = None
|
config: DeviceConfig | None = None
|
||||||
if config_dict := entry.data.get(CONF_DEVICE_CONFIG):
|
if config_dict := entry.data.get(CONF_DEVICE_CONFIG):
|
||||||
@ -151,19 +153,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
|||||||
config.timeout = CONNECT_TIMEOUT
|
config.timeout = CONNECT_TIMEOUT
|
||||||
if config.uses_http is True:
|
if config.uses_http is True:
|
||||||
config.http_client = create_async_tplink_clientsession(hass)
|
config.http_client = create_async_tplink_clientsession(hass)
|
||||||
|
|
||||||
|
# If we have in memory credentials use them otherwise check for credentials_hash
|
||||||
if credentials:
|
if credentials:
|
||||||
config.credentials = credentials
|
config.credentials = credentials
|
||||||
|
elif entry_credentials_hash:
|
||||||
|
config.credentials_hash = entry_credentials_hash
|
||||||
|
|
||||||
try:
|
try:
|
||||||
device: Device = await Device.connect(config=config)
|
device: Device = await Device.connect(config=config)
|
||||||
except AuthenticationError as ex:
|
except AuthenticationError as ex:
|
||||||
|
# If the stored credentials_hash was used but doesn't work remove it
|
||||||
|
if not credentials and entry_credentials_hash:
|
||||||
|
data = {k: v for k, v in entry.data.items() if k != CONF_CREDENTIALS_HASH}
|
||||||
|
hass.config_entries.async_update_entry(entry, data=data)
|
||||||
raise ConfigEntryAuthFailed from ex
|
raise ConfigEntryAuthFailed from ex
|
||||||
except KasaException as ex:
|
except KasaException as ex:
|
||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady from ex
|
||||||
|
|
||||||
device_config_dict = device.config.to_dict(
|
device_credentials_hash = device.credentials_hash
|
||||||
credentials_hash=device.credentials_hash, exclude_credentials=True
|
device_config_dict = device.config.to_dict(exclude_credentials=True)
|
||||||
)
|
# Do not store the credentials hash inside the device_config
|
||||||
|
device_config_dict.pop(CONF_CREDENTIALS_HASH, None)
|
||||||
updates: dict[str, Any] = {}
|
updates: dict[str, Any] = {}
|
||||||
|
if device_credentials_hash and device_credentials_hash != entry_credentials_hash:
|
||||||
|
updates[CONF_CREDENTIALS_HASH] = device_credentials_hash
|
||||||
if device_config_dict != config_dict:
|
if device_config_dict != config_dict:
|
||||||
updates[CONF_DEVICE_CONFIG] = device_config_dict
|
updates[CONF_DEVICE_CONFIG] = device_config_dict
|
||||||
if entry.data.get(CONF_ALIAS) != device.alias:
|
if entry.data.get(CONF_ALIAS) != device.alias:
|
||||||
@ -326,7 +340,25 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
|
|
||||||
minor_version = 3
|
minor_version = 3
|
||||||
hass.config_entries.async_update_entry(config_entry, minor_version=3)
|
hass.config_entries.async_update_entry(config_entry, minor_version=3)
|
||||||
|
|
||||||
_LOGGER.debug("Migration to version %s.%s successful", version, minor_version)
|
_LOGGER.debug("Migration to version %s.%s successful", version, minor_version)
|
||||||
|
|
||||||
|
if version == 1 and minor_version == 3:
|
||||||
|
# credentials_hash stored in the device_config should be moved to data.
|
||||||
|
updates: dict[str, Any] = {}
|
||||||
|
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
|
||||||
|
assert isinstance(config_dict, dict)
|
||||||
|
if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None):
|
||||||
|
updates[CONF_CREDENTIALS_HASH] = credentials_hash
|
||||||
|
updates[CONF_DEVICE_CONFIG] = config_dict
|
||||||
|
minor_version = 4
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
config_entry,
|
||||||
|
data={
|
||||||
|
**config_entry.data,
|
||||||
|
**updates,
|
||||||
|
},
|
||||||
|
minor_version=minor_version,
|
||||||
|
)
|
||||||
|
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -44,7 +44,13 @@ from . import (
|
|||||||
mac_alias,
|
mac_alias,
|
||||||
set_credentials,
|
set_credentials,
|
||||||
)
|
)
|
||||||
from .const import CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DOMAIN
|
from .const import (
|
||||||
|
CONF_CONNECTION_TYPE,
|
||||||
|
CONF_CREDENTIALS_HASH,
|
||||||
|
CONF_DEVICE_CONFIG,
|
||||||
|
CONNECT_TIMEOUT,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
STEP_AUTH_DATA_SCHEMA = vol.Schema(
|
STEP_AUTH_DATA_SCHEMA = vol.Schema(
|
||||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||||
@ -55,7 +61,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for tplink."""
|
"""Handle a config flow for tplink."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
MINOR_VERSION = 3
|
MINOR_VERSION = 4
|
||||||
reauth_entry: ConfigEntry | None = None
|
reauth_entry: ConfigEntry | None = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -95,9 +101,18 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG)
|
entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG)
|
||||||
if entry_config_dict == config and entry_data[CONF_HOST] == host:
|
if entry_config_dict == config and entry_data[CONF_HOST] == host:
|
||||||
return None
|
return None
|
||||||
|
updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host}
|
||||||
|
# If the connection parameters have changed the credentials_hash will be invalid.
|
||||||
|
if (
|
||||||
|
entry_config_dict
|
||||||
|
and isinstance(entry_config_dict, dict)
|
||||||
|
and entry_config_dict.get(CONF_CONNECTION_TYPE)
|
||||||
|
!= config.get(CONF_CONNECTION_TYPE)
|
||||||
|
):
|
||||||
|
updates.pop(CONF_CREDENTIALS_HASH, None)
|
||||||
return self.async_update_reload_and_abort(
|
return self.async_update_reload_and_abort(
|
||||||
entry,
|
entry,
|
||||||
data={**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host},
|
data=updates,
|
||||||
reason="already_configured",
|
reason="already_configured",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -345,18 +360,22 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
@callback
|
@callback
|
||||||
def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult:
|
def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult:
|
||||||
"""Create a config entry from a smart device."""
|
"""Create a config entry from a smart device."""
|
||||||
|
# This is only ever called after a successful device update so we know that
|
||||||
|
# the credential_hash is correct and should be saved.
|
||||||
self._abort_if_unique_id_configured(updates={CONF_HOST: device.host})
|
self._abort_if_unique_id_configured(updates={CONF_HOST: device.host})
|
||||||
|
data = {
|
||||||
|
CONF_HOST: device.host,
|
||||||
|
CONF_ALIAS: device.alias,
|
||||||
|
CONF_MODEL: device.model,
|
||||||
|
CONF_DEVICE_CONFIG: device.config.to_dict(
|
||||||
|
exclude_credentials=True,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if device.credentials_hash:
|
||||||
|
data[CONF_CREDENTIALS_HASH] = device.credentials_hash
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=f"{device.alias} {device.model}",
|
title=f"{device.alias} {device.model}",
|
||||||
data={
|
data=data,
|
||||||
CONF_HOST: device.host,
|
|
||||||
CONF_ALIAS: device.alias,
|
|
||||||
CONF_MODEL: device.model,
|
|
||||||
CONF_DEVICE_CONFIG: device.config.to_dict(
|
|
||||||
credentials_hash=device.credentials_hash,
|
|
||||||
exclude_credentials=True,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_try_discover_and_update(
|
async def _async_try_discover_and_update(
|
||||||
|
@ -20,6 +20,8 @@ ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh"
|
|||||||
ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh"
|
ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh"
|
||||||
|
|
||||||
CONF_DEVICE_CONFIG: Final = "device_config"
|
CONF_DEVICE_CONFIG: Final = "device_config"
|
||||||
|
CONF_CREDENTIALS_HASH: Final = "credentials_hash"
|
||||||
|
CONF_CONNECTION_TYPE: Final = "connection_type"
|
||||||
|
|
||||||
PLATFORMS: Final = [
|
PLATFORMS: Final = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
|
@ -22,6 +22,7 @@ from syrupy import SnapshotAssertion
|
|||||||
|
|
||||||
from homeassistant.components.tplink import (
|
from homeassistant.components.tplink import (
|
||||||
CONF_ALIAS,
|
CONF_ALIAS,
|
||||||
|
CONF_CREDENTIALS_HASH,
|
||||||
CONF_DEVICE_CONFIG,
|
CONF_DEVICE_CONFIG,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
@ -53,9 +54,7 @@ MAC_ADDRESS2 = "11:22:33:44:55:66"
|
|||||||
DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}"
|
DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}"
|
||||||
CREDENTIALS_HASH_LEGACY = ""
|
CREDENTIALS_HASH_LEGACY = ""
|
||||||
DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS)
|
DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS)
|
||||||
DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(
|
DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(exclude_credentials=True)
|
||||||
credentials_hash=CREDENTIALS_HASH_LEGACY, exclude_credentials=True
|
|
||||||
)
|
|
||||||
CREDENTIALS = Credentials("foo", "bar")
|
CREDENTIALS = Credentials("foo", "bar")
|
||||||
CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv=="
|
CREDENTIALS_HASH_AUTH = "abcdefghijklmnopqrstuv=="
|
||||||
DEVICE_CONFIG_AUTH = DeviceConfig(
|
DEVICE_CONFIG_AUTH = DeviceConfig(
|
||||||
@ -74,12 +73,8 @@ DEVICE_CONFIG_AUTH2 = DeviceConfig(
|
|||||||
),
|
),
|
||||||
uses_http=True,
|
uses_http=True,
|
||||||
)
|
)
|
||||||
DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict(
|
DEVICE_CONFIG_DICT_AUTH = DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)
|
||||||
credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True
|
DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict(exclude_credentials=True)
|
||||||
)
|
|
||||||
DEVICE_CONFIG_DICT_AUTH2 = DEVICE_CONFIG_AUTH2.to_dict(
|
|
||||||
credentials_hash=CREDENTIALS_HASH_AUTH, exclude_credentials=True
|
|
||||||
)
|
|
||||||
|
|
||||||
CREATE_ENTRY_DATA_LEGACY = {
|
CREATE_ENTRY_DATA_LEGACY = {
|
||||||
CONF_HOST: IP_ADDRESS,
|
CONF_HOST: IP_ADDRESS,
|
||||||
@ -92,14 +87,20 @@ CREATE_ENTRY_DATA_AUTH = {
|
|||||||
CONF_HOST: IP_ADDRESS,
|
CONF_HOST: IP_ADDRESS,
|
||||||
CONF_ALIAS: ALIAS,
|
CONF_ALIAS: ALIAS,
|
||||||
CONF_MODEL: MODEL,
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH,
|
||||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH,
|
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH,
|
||||||
}
|
}
|
||||||
CREATE_ENTRY_DATA_AUTH2 = {
|
CREATE_ENTRY_DATA_AUTH2 = {
|
||||||
CONF_HOST: IP_ADDRESS2,
|
CONF_HOST: IP_ADDRESS2,
|
||||||
CONF_ALIAS: ALIAS,
|
CONF_ALIAS: ALIAS,
|
||||||
CONF_MODEL: MODEL,
|
CONF_MODEL: MODEL,
|
||||||
|
CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AUTH,
|
||||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH2,
|
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AUTH2,
|
||||||
}
|
}
|
||||||
|
NEW_CONNECTION_TYPE = DeviceConnectionParameters(
|
||||||
|
DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Aes
|
||||||
|
)
|
||||||
|
NEW_CONNECTION_TYPE_DICT = NEW_CONNECTION_TYPE.to_dict()
|
||||||
|
|
||||||
|
|
||||||
def _load_feature_fixtures():
|
def _load_feature_fixtures():
|
||||||
|
@ -14,8 +14,12 @@ from homeassistant.components.tplink import (
|
|||||||
DeviceConfig,
|
DeviceConfig,
|
||||||
KasaException,
|
KasaException,
|
||||||
)
|
)
|
||||||
from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG
|
from homeassistant.components.tplink.const import (
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
CONF_CONNECTION_TYPE,
|
||||||
|
CONF_CREDENTIALS_HASH,
|
||||||
|
CONF_DEVICE_CONFIG,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ALIAS,
|
CONF_ALIAS,
|
||||||
CONF_DEVICE,
|
CONF_DEVICE,
|
||||||
@ -32,6 +36,7 @@ from . import (
|
|||||||
CREATE_ENTRY_DATA_AUTH,
|
CREATE_ENTRY_DATA_AUTH,
|
||||||
CREATE_ENTRY_DATA_AUTH2,
|
CREATE_ENTRY_DATA_AUTH2,
|
||||||
CREATE_ENTRY_DATA_LEGACY,
|
CREATE_ENTRY_DATA_LEGACY,
|
||||||
|
CREDENTIALS_HASH_AUTH,
|
||||||
DEFAULT_ENTRY_TITLE,
|
DEFAULT_ENTRY_TITLE,
|
||||||
DEVICE_CONFIG_DICT_AUTH,
|
DEVICE_CONFIG_DICT_AUTH,
|
||||||
DEVICE_CONFIG_DICT_LEGACY,
|
DEVICE_CONFIG_DICT_LEGACY,
|
||||||
@ -40,6 +45,7 @@ from . import (
|
|||||||
MAC_ADDRESS,
|
MAC_ADDRESS,
|
||||||
MAC_ADDRESS2,
|
MAC_ADDRESS2,
|
||||||
MODULE,
|
MODULE,
|
||||||
|
NEW_CONNECTION_TYPE_DICT,
|
||||||
_mocked_device,
|
_mocked_device,
|
||||||
_patch_connect,
|
_patch_connect,
|
||||||
_patch_discovery,
|
_patch_discovery,
|
||||||
@ -811,6 +817,77 @@ async def test_integration_discovery_with_ip_change(
|
|||||||
mock_connect["connect"].assert_awaited_once_with(config=config)
|
mock_connect["connect"].assert_awaited_once_with(config=config)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_integration_discovery_with_connection_change(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_discovery: AsyncMock,
|
||||||
|
mock_connect: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that config entry is updated with new device config.
|
||||||
|
|
||||||
|
And that connection_hash is removed as it will be invalid.
|
||||||
|
"""
|
||||||
|
mock_connect["connect"].side_effect = KasaException()
|
||||||
|
|
||||||
|
mock_config_entry = MockConfigEntry(
|
||||||
|
title="TPLink",
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=CREATE_ENTRY_DATA_AUTH,
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
)
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
assert (
|
||||||
|
len(
|
||||||
|
hass.config_entries.flow.async_progress_by_handler(
|
||||||
|
DOMAIN, match_context={"source": SOURCE_REAUTH}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AUTH
|
||||||
|
assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1"
|
||||||
|
assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AUTH
|
||||||
|
|
||||||
|
NEW_DEVICE_CONFIG = {
|
||||||
|
**DEVICE_CONFIG_DICT_AUTH,
|
||||||
|
CONF_CONNECTION_TYPE: NEW_CONNECTION_TYPE_DICT,
|
||||||
|
}
|
||||||
|
config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG)
|
||||||
|
# Reset the connect mock so when the config flow reloads the entry it succeeds
|
||||||
|
mock_connect["connect"].reset_mock(side_effect=True)
|
||||||
|
bulb = _mocked_device(
|
||||||
|
device_config=config,
|
||||||
|
mac=mock_config_entry.unique_id,
|
||||||
|
)
|
||||||
|
mock_connect["connect"].return_value = bulb
|
||||||
|
|
||||||
|
discovery_result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||||
|
data={
|
||||||
|
CONF_HOST: "127.0.0.1",
|
||||||
|
CONF_MAC: MAC_ADDRESS,
|
||||||
|
CONF_ALIAS: ALIAS,
|
||||||
|
CONF_DEVICE_CONFIG: NEW_DEVICE_CONFIG,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
assert discovery_result["type"] is FlowResultType.ABORT
|
||||||
|
assert discovery_result["reason"] == "already_configured"
|
||||||
|
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == NEW_DEVICE_CONFIG
|
||||||
|
assert mock_config_entry.data[CONF_HOST] == "127.0.0.1"
|
||||||
|
assert CREDENTIALS_HASH_AUTH not in mock_config_entry.data
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
mock_connect["connect"].assert_awaited_once_with(config=config)
|
||||||
|
|
||||||
|
|
||||||
async def test_dhcp_discovery_with_ip_change(
|
async def test_dhcp_discovery_with_ip_change(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
|
@ -7,12 +7,16 @@ from datetime import timedelta
|
|||||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
from kasa import AuthenticationError, Feature, KasaException, Module
|
from kasa import AuthenticationError, DeviceConfig, Feature, KasaException, Module
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import setup
|
from homeassistant import setup
|
||||||
from homeassistant.components import tplink
|
from homeassistant.components import tplink
|
||||||
from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG, DOMAIN
|
from homeassistant.components.tplink.const import (
|
||||||
|
CONF_CREDENTIALS_HASH,
|
||||||
|
CONF_DEVICE_CONFIG,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_AUTHENTICATION,
|
CONF_AUTHENTICATION,
|
||||||
@ -458,7 +462,214 @@ async def test_unlink_devices(
|
|||||||
expected_identifiers = identifiers[:expected_count]
|
expected_identifiers = identifiers[:expected_count]
|
||||||
assert device_entries[0].identifiers == set(expected_identifiers)
|
assert device_entries[0].identifiers == set(expected_identifiers)
|
||||||
assert entry.version == 1
|
assert entry.version == 1
|
||||||
assert entry.minor_version == 3
|
assert entry.minor_version == 4
|
||||||
|
|
||||||
msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}"
|
msg = f"{expected_message} identifiers for device dummy (hs300): {set(identifiers)}"
|
||||||
assert msg in caplog.text
|
assert msg in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_move_credentials_hash(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test credentials hash moved to parent.
|
||||||
|
|
||||||
|
As async_setup_entry will succeed the hash on the parent is updated
|
||||||
|
from the device.
|
||||||
|
"""
|
||||||
|
device_config = {
|
||||||
|
**DEVICE_CONFIG_AUTH.to_dict(
|
||||||
|
exclude_credentials=True, credentials_hash="theHash"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
title="TPLink",
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=entry_data,
|
||||||
|
entry_id="123456",
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
version=1,
|
||||||
|
minor_version=3,
|
||||||
|
)
|
||||||
|
assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash"
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
async def _connect(config):
|
||||||
|
config.credentials_hash = "theNewHash"
|
||||||
|
return _mocked_device(device_config=config, credentials_hash="theNewHash")
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.tplink.Device.connect", new=_connect),
|
||||||
|
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.minor_version == 4
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG]
|
||||||
|
assert CONF_CREDENTIALS_HASH in entry.data
|
||||||
|
# Gets the new hash from the successful connection.
|
||||||
|
assert entry.data[CONF_CREDENTIALS_HASH] == "theNewHash"
|
||||||
|
assert "Migration to version 1.4 complete" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_move_credentials_hash_auth_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test credentials hash moved to parent.
|
||||||
|
|
||||||
|
If there is an auth error it should be deleted after migration
|
||||||
|
in async_setup_entry.
|
||||||
|
"""
|
||||||
|
device_config = {
|
||||||
|
**DEVICE_CONFIG_AUTH.to_dict(
|
||||||
|
exclude_credentials=True, credentials_hash="theHash"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
title="TPLink",
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=entry_data,
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
version=1,
|
||||||
|
minor_version=3,
|
||||||
|
)
|
||||||
|
assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.tplink.Device.connect",
|
||||||
|
side_effect=AuthenticationError,
|
||||||
|
),
|
||||||
|
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||||
|
):
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.minor_version == 4
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||||
|
assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG]
|
||||||
|
# Auth failure deletes the hash
|
||||||
|
assert CONF_CREDENTIALS_HASH not in entry.data
|
||||||
|
|
||||||
|
|
||||||
|
async def test_move_credentials_hash_other_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test credentials hash moved to parent.
|
||||||
|
|
||||||
|
When there is a KasaException the same hash should still be on the parent
|
||||||
|
at the end of the test.
|
||||||
|
"""
|
||||||
|
device_config = {
|
||||||
|
**DEVICE_CONFIG_AUTH.to_dict(
|
||||||
|
exclude_credentials=True, credentials_hash="theHash"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry_data = {**CREATE_ENTRY_DATA_AUTH, CONF_DEVICE_CONFIG: device_config}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
title="TPLink",
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=entry_data,
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
version=1,
|
||||||
|
minor_version=3,
|
||||||
|
)
|
||||||
|
assert entry.data[CONF_DEVICE_CONFIG][CONF_CREDENTIALS_HASH] == "theHash"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.tplink.Device.connect", side_effect=KasaException
|
||||||
|
),
|
||||||
|
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||||
|
):
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.minor_version == 4
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG]
|
||||||
|
assert CONF_CREDENTIALS_HASH in entry.data
|
||||||
|
assert entry.data[CONF_CREDENTIALS_HASH] == "theHash"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_credentials_hash(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test credentials_hash used to call connect."""
|
||||||
|
device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)}
|
||||||
|
entry_data = {
|
||||||
|
**CREATE_ENTRY_DATA_AUTH,
|
||||||
|
CONF_DEVICE_CONFIG: device_config,
|
||||||
|
CONF_CREDENTIALS_HASH: "theHash",
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
title="TPLink",
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=entry_data,
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _connect(config):
|
||||||
|
config.credentials_hash = "theHash"
|
||||||
|
return _mocked_device(device_config=config, credentials_hash="theHash")
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||||
|
patch("homeassistant.components.tplink.Device.connect", new=_connect),
|
||||||
|
):
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG]
|
||||||
|
assert CONF_CREDENTIALS_HASH in entry.data
|
||||||
|
assert entry.data[CONF_DEVICE_CONFIG] == device_config
|
||||||
|
assert entry.data[CONF_CREDENTIALS_HASH] == "theHash"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_credentials_hash_auth_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test credentials_hash is deleted after an auth failure."""
|
||||||
|
device_config = {**DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True)}
|
||||||
|
entry_data = {
|
||||||
|
**CREATE_ENTRY_DATA_AUTH,
|
||||||
|
CONF_DEVICE_CONFIG: device_config,
|
||||||
|
CONF_CREDENTIALS_HASH: "theHash",
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
title="TPLink",
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=entry_data,
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.tplink.Device.connect",
|
||||||
|
side_effect=AuthenticationError,
|
||||||
|
) as connect_mock,
|
||||||
|
):
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
expected_config = DeviceConfig.from_dict(
|
||||||
|
DEVICE_CONFIG_AUTH.to_dict(exclude_credentials=True, credentials_hash="theHash")
|
||||||
|
)
|
||||||
|
connect_mock.assert_called_with(config=expected_config)
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||||
|
assert CONF_CREDENTIALS_HASH not in entry.data
|
||||||
|
Loading…
x
Reference in New Issue
Block a user