Migrate entry unique id for Sensibo (#67119)

This commit is contained in:
G Johansson 2022-02-27 22:38:39 +01:00 committed by GitHub
parent 9c440d8aa6
commit bb4b7c96d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 212 additions and 74 deletions

View File

@ -1,11 +1,15 @@
"""The sensibo component.""" """The sensibo component."""
from __future__ import annotations from __future__ import annotations
from pysensibo.exceptions import AuthenticationError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN, PLATFORMS from .const import DOMAIN, LOGGER, PLATFORMS
from .coordinator import SensiboDataUpdateCoordinator from .coordinator import SensiboDataUpdateCoordinator
from .util import NoDevicesError, NoUsernameError, async_validate_api
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -28,3 +32,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
del hass.data[DOMAIN] del hass.data[DOMAIN]
return True return True
return False return False
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""
# Change entry unique id from api_key to username
if entry.version == 1:
api_key = entry.data[CONF_API_KEY]
try:
new_unique_id = await async_validate_api(hass, api_key)
except (AuthenticationError, ConnectionError, NoDevicesError, NoUsernameError):
return False
entry.version = 2
LOGGER.debug("Migrate Sensibo config entry unique id to %s", new_unique_id)
hass.config_entries.async_update_entry(
entry,
unique_id=new_unique_id,
)
return True

View File

@ -1,25 +1,16 @@
"""Adds config flow for Sensibo integration.""" """Adds config flow for Sensibo integration."""
from __future__ import annotations from __future__ import annotations
import asyncio from pysensibo.exceptions import AuthenticationError
import logging
import aiohttp
import async_timeout
from pysensibo import SensiboClient
from pysensibo.exceptions import AuthenticationError, SensiboError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .const import DEFAULT_NAME, DOMAIN, TIMEOUT from .const import DEFAULT_NAME, DOMAIN
from .util import NoDevicesError, NoUsernameError, async_validate_api
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema( DATA_SCHEMA = vol.Schema(
{ {
@ -28,39 +19,14 @@ DATA_SCHEMA = vol.Schema(
) )
async def async_validate_api(hass: HomeAssistant, api_key: str) -> bool:
"""Get data from API."""
client = SensiboClient(
api_key,
session=async_get_clientsession(hass),
timeout=TIMEOUT,
)
try:
async with async_timeout.timeout(TIMEOUT):
if await client.async_get_devices():
return True
except (
aiohttp.ClientConnectionError,
asyncio.TimeoutError,
AuthenticationError,
SensiboError,
) as err:
_LOGGER.error("Failed to get devices from Sensibo servers %s", err)
return False
class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sensibo integration.""" """Handle a config flow for Sensibo integration."""
VERSION = 1 VERSION = 2
async def async_step_import(self, config: dict) -> FlowResult: async def async_step_import(self, config: dict) -> FlowResult:
"""Import a configuration from config.yaml.""" """Import a configuration from config.yaml."""
self.context.update(
{"title_placeholders": {"Sensibo": f"YAML import {DOMAIN}"}}
)
return await self.async_step_user(user_input=config) return await self.async_step_user(user_input=config)
async def async_step_user(self, user_input=None) -> FlowResult: async def async_step_user(self, user_input=None) -> FlowResult:
@ -71,17 +37,24 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input: if user_input:
api_key = user_input[CONF_API_KEY] api_key = user_input[CONF_API_KEY]
try:
username = await async_validate_api(self.hass, api_key)
except AuthenticationError:
errors["base"] = "invalid_auth"
except ConnectionError:
errors["base"] = "cannot_connect"
except NoDevicesError:
errors["base"] = "no_devices"
except NoUsernameError:
errors["base"] = "no_username"
else:
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
await self.async_set_unique_id(api_key)
self._abort_if_unique_id_configured()
validate = await async_validate_api(self.hass, api_key)
if validate:
return self.async_create_entry( return self.async_create_entry(
title=DEFAULT_NAME, title=DEFAULT_NAME,
data={CONF_API_KEY: api_key}, data={CONF_API_KEY: api_key},
) )
errors["base"] = "cannot_connect"
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",

View File

@ -3,8 +3,11 @@
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]" "already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}, },
"error":{ "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"no_devices": "No devices discovered",
"no_username": "Could not get username"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -4,7 +4,10 @@
"already_configured": "Account is already configured" "already_configured": "Account is already configured"
}, },
"error": { "error": {
"cannot_connect": "Failed to connect" "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"no_devices": "No devices discovered",
"no_username": "Could not get username"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -0,0 +1,49 @@
"""Utils for Sensibo integration."""
from __future__ import annotations
import async_timeout
from pysensibo import SensiboClient
from pysensibo.exceptions import AuthenticationError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import LOGGER, SENSIBO_ERRORS, TIMEOUT
async def async_validate_api(hass: HomeAssistant, api_key: str) -> str:
"""Get data from API."""
client = SensiboClient(
api_key,
session=async_get_clientsession(hass),
timeout=TIMEOUT,
)
try:
async with async_timeout.timeout(TIMEOUT):
device_query = await client.async_get_devices()
user_query = await client.async_get_me()
except AuthenticationError as err:
LOGGER.error("Could not authenticate on Sensibo servers %s", err)
raise AuthenticationError from err
except SENSIBO_ERRORS as err:
LOGGER.error("Failed to get information from Sensibo servers %s", err)
raise ConnectionError from err
devices = device_query["result"]
user = user_query["result"].get("username")
if not devices:
LOGGER.error("Could not retrieve any devices from Sensibo servers")
raise NoDevicesError
if not user:
LOGGER.error("Could not retrieve username from Sensibo servers")
raise NoUsernameError
return user
class NoDevicesError(Exception):
"""No devices from Sensibo api."""
class NoUsernameError(Exception):
"""No username from Sensibo api."""

View File

@ -5,7 +5,7 @@ import asyncio
from unittest.mock import patch from unittest.mock import patch
import aiohttp import aiohttp
from pysensibo import AuthenticationError, SensiboError from pysensibo.exceptions import AuthenticationError, SensiboError
import pytest import pytest
from homeassistant import config_entries from homeassistant import config_entries
@ -22,11 +22,6 @@ from tests.common import MockConfigEntry
DOMAIN = "sensibo" DOMAIN = "sensibo"
def devices():
"""Return list of test devices."""
return (yield from [{"id": "xyzxyz"}, {"id": "abcabc"}])
async def test_form(hass: HomeAssistant) -> None: async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form.""" """Test we get the form."""
@ -38,8 +33,11 @@ async def test_form(hass: HomeAssistant) -> None:
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch(
"homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", "homeassistant.components.sensibo.util.SensiboClient.async_get_devices",
return_value=devices(), return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]},
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_me",
return_value={"result": {"username": "username"}},
), patch( ), patch(
"homeassistant.components.sensibo.async_setup_entry", "homeassistant.components.sensibo.async_setup_entry",
return_value=True, return_value=True,
@ -53,6 +51,7 @@ async def test_form(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["version"] == 2
assert result2["data"] == { assert result2["data"] == {
"api_key": "1234567890", "api_key": "1234567890",
} }
@ -64,8 +63,11 @@ async def test_import_flow_success(hass: HomeAssistant) -> None:
"""Test a successful import of yaml.""" """Test a successful import of yaml."""
with patch( with patch(
"homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", "homeassistant.components.sensibo.util.SensiboClient.async_get_devices",
return_value=devices(), return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]},
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_me",
return_value={"result": {"username": "username"}},
), patch( ), patch(
"homeassistant.components.sensibo.async_setup_entry", "homeassistant.components.sensibo.async_setup_entry",
return_value=True, return_value=True,
@ -95,15 +97,18 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None:
data={ data={
CONF_API_KEY: "1234567890", CONF_API_KEY: "1234567890",
}, },
unique_id="1234567890", unique_id="username",
).add_to_hass(hass) ).add_to_hass(hass)
with patch( with patch(
"homeassistant.components.sensibo.async_setup_entry", "homeassistant.components.sensibo.async_setup_entry",
return_value=True, return_value=True,
), patch( ), patch(
"homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", "homeassistant.components.sensibo.util.SensiboClient.async_get_devices",
return_value=devices(), return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]},
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_me",
return_value={"result": {"username": "username"}},
): ):
result3 = await hass.config_entries.flow.async_init( result3 = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -119,33 +124,112 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"error_message", "error_message, p_error",
[ [
(aiohttp.ClientConnectionError), (aiohttp.ClientConnectionError, "cannot_connect"),
(asyncio.TimeoutError), (asyncio.TimeoutError, "cannot_connect"),
(AuthenticationError), (AuthenticationError, "invalid_auth"),
(SensiboError), (SensiboError, "cannot_connect"),
], ],
) )
async def test_flow_fails(hass: HomeAssistant, error_message) -> None: async def test_flow_fails(
hass: HomeAssistant, error_message: Exception, p_error: str
) -> None:
"""Test config flow errors.""" """Test config flow errors."""
result4 = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result4["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert result4["step_id"] == config_entries.SOURCE_USER assert result["step_id"] == config_entries.SOURCE_USER
with patch( with patch(
"homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", "homeassistant.components.sensibo.util.SensiboClient.async_get_devices",
side_effect=error_message, side_effect=error_message,
): ):
result4 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result4["flow_id"], result["flow_id"],
user_input={ user_input={
CONF_API_KEY: "1234567890", CONF_API_KEY: "1234567890",
}, },
) )
assert result4["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": p_error}
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices",
return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]},
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_me",
return_value={"result": {"username": "username"}},
), patch(
"homeassistant.components.sensibo.async_setup_entry",
return_value=True,
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_API_KEY: "1234567891",
},
)
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "Sensibo"
assert result3["data"] == {
"api_key": "1234567891",
}
async def test_flow_get_no_devices(hass: HomeAssistant) -> None:
"""Test config flow get no devices from api."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == config_entries.SOURCE_USER
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices",
return_value={"result": []},
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_me",
return_value={"result": {}},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_API_KEY: "1234567890",
},
)
assert result2["errors"] == {"base": "no_devices"}
async def test_flow_get_no_username(hass: HomeAssistant) -> None:
"""Test config flow get no username from api."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == config_entries.SOURCE_USER
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices",
return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]},
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_me",
return_value={"result": {}},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_API_KEY: "1234567890",
},
)
assert result2["errors"] == {"base": "no_username"}