Add unique ID to config entry in Luftdaten (#62176)

This commit is contained in:
Franck Nijhof 2021-12-19 12:42:52 +01:00 committed by GitHub
parent b559d8845e
commit 7fe895e554
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 97 deletions

View File

@ -7,7 +7,6 @@ from luftdaten import Luftdaten
from luftdaten.exceptions import LuftdatenError from luftdaten.exceptions import LuftdatenError
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONF_MONITORED_CONDITIONS, CONF_MONITORED_CONDITIONS,
@ -18,13 +17,11 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
Platform, Platform,
) )
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from .config_flow import duplicate_stations
from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -83,13 +80,6 @@ SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@callback
def _async_fixup_sensor_id(hass, config_entry, sensor_id):
hass.config_entries.async_update_entry(
config_entry, data={**config_entry.data, CONF_SENSOR_ID: int(sensor_id)}
)
async def async_setup_entry(hass, config_entry): async def async_setup_entry(hass, config_entry):
"""Set up Luftdaten as config entry.""" """Set up Luftdaten as config entry."""
hass.data.setdefault( hass.data.setdefault(
@ -100,19 +90,11 @@ async def async_setup_entry(hass, config_entry):
}, },
) )
if not isinstance(config_entry.data[CONF_SENSOR_ID], int): # For backwards compat, set unique ID
_async_fixup_sensor_id(hass, config_entry, config_entry.data[CONF_SENSOR_ID]) if config_entry.unique_id is None:
hass.config_entries.async_update_entry(
if ( config_entry, unique_id=config_entry.data[CONF_SENSOR_ID]
config_entry.data[CONF_SENSOR_ID] in duplicate_stations(hass)
and config_entry.source == SOURCE_IMPORT
):
_LOGGER.warning(
"Removing duplicate sensors for station %s",
config_entry.data[CONF_SENSOR_ID],
) )
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
try: try:
luftdaten = LuftDatenData( luftdaten = LuftDatenData(

View File

@ -18,25 +18,6 @@ import homeassistant.helpers.config_validation as cv
from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN
@callback
def configured_sensors(hass):
"""Return a set of configured Luftdaten sensors."""
return {
entry.data[CONF_SENSOR_ID]
for entry in hass.config_entries.async_entries(DOMAIN)
}
@callback
def duplicate_stations(hass):
"""Return a set of duplicate configured Luftdaten stations."""
stations = [
int(entry.data[CONF_SENSOR_ID])
for entry in hass.config_entries.async_entries(DOMAIN)
]
return {x for x in stations if stations.count(x) > 1}
class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Luftdaten config flow.""" """Handle a Luftdaten config flow."""
@ -59,10 +40,8 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if not user_input: if not user_input:
return self._show_form() return self._show_form()
sensor_id = user_input[CONF_SENSOR_ID] await self.async_set_unique_id(str(user_input[CONF_SENSOR_ID]))
self._abort_if_unique_id_configured()
if sensor_id in configured_sensors(self.hass):
return self._show_form({CONF_SENSOR_ID: "already_configured"})
luftdaten = Luftdaten(user_input[CONF_SENSOR_ID]) luftdaten = Luftdaten(user_input[CONF_SENSOR_ID])
try: try:
@ -86,4 +65,6 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
user_input.update({CONF_SCAN_INTERVAL: scan_interval.total_seconds()}) user_input.update({CONF_SCAN_INTERVAL: scan_interval.total_seconds()})
return self.async_create_entry(title=str(sensor_id), data=user_input) return self.async_create_entry(
title=str(user_input[CONF_SENSOR_ID]), data=user_input
)

View File

@ -0,0 +1,32 @@
"""Fixtures for Luftdaten tests."""
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import patch
import pytest
from homeassistant.components.luftdaten import DOMAIN
from homeassistant.components.luftdaten.const import CONF_SENSOR_ID
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="12345",
domain=DOMAIN,
data={CONF_SENSOR_ID: 123456},
unique_id="12345",
)
@pytest.fixture
def mock_setup_entry() -> Generator[None, None, None]:
"""Mock setting up a config entry."""
with patch(
"homeassistant.components.luftdaten.async_setup_entry", return_value=True
):
yield

View File

@ -1,84 +1,114 @@
"""Define tests for the Luftdaten config flow.""" """Define tests for the Luftdaten config flow."""
from datetime import timedelta from unittest.mock import MagicMock, patch
from unittest.mock import patch
from homeassistant import data_entry_flow from luftdaten.exceptions import LuftdatenConnectionError
from homeassistant.components.luftdaten import DOMAIN, config_flow
from homeassistant.components.luftdaten import DOMAIN
from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.components.luftdaten.const import CONF_SENSOR_ID
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_duplicate_error(hass): async def test_duplicate_error(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test that errors are shown when duplicates are added.""" """Test that errors are shown when duplicates are added."""
conf = {CONF_SENSOR_ID: "12345abcde"} mock_config_entry.add_to_hass(hass)
MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) result = await hass.config_entries.flow.async_init(
flow = config_flow.LuftDatenFlowHandler() DOMAIN, context={"source": SOURCE_USER}
flow.hass = hass )
result = await flow.async_step_user(user_input=conf) assert result.get("type") == RESULT_TYPE_FORM
assert result["errors"] == {CONF_SENSOR_ID: "already_configured"} assert result.get("step_id") == SOURCE_USER
assert "flow_id" in result
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_SENSOR_ID: 12345},
)
assert result2.get("type") == RESULT_TYPE_ABORT
assert result2.get("reason") == "already_configured"
async def test_communication_error(hass): async def test_communication_error(hass: HomeAssistant) -> None:
"""Test that no sensor is added while unable to communicate with API.""" """Test that no sensor is added while unable to communicate with API."""
conf = {CONF_SENSOR_ID: "12345abcde"} result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
flow = config_flow.LuftDatenFlowHandler() assert result.get("type") == RESULT_TYPE_FORM
flow.hass = hass assert result.get("step_id") == SOURCE_USER
assert "flow_id" in result
with patch("luftdaten.Luftdaten.get_data", return_value=None): with patch("luftdaten.Luftdaten.get_data", side_effect=LuftdatenConnectionError):
result = await flow.async_step_user(user_input=conf) result2 = await hass.config_entries.flow.async_configure(
assert result["errors"] == {CONF_SENSOR_ID: "invalid_sensor"} result["flow_id"],
user_input={CONF_SENSOR_ID: 12345},
)
assert result2.get("type") == RESULT_TYPE_FORM
assert result2.get("step_id") == SOURCE_USER
assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"}
async def test_invalid_sensor(hass): async def test_invalid_sensor(hass: HomeAssistant) -> None:
"""Test that an invalid sensor throws an error.""" """Test that an invalid sensor throws an error."""
conf = {CONF_SENSOR_ID: "12345abcde"} result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
flow = config_flow.LuftDatenFlowHandler() assert result.get("type") == RESULT_TYPE_FORM
flow.hass = hass assert result.get("step_id") == SOURCE_USER
assert "flow_id" in result
with patch("luftdaten.Luftdaten.get_data", return_value=False), patch( with patch("luftdaten.Luftdaten.get_data", return_value=False), patch(
"luftdaten.Luftdaten.validate_sensor", return_value=False "luftdaten.Luftdaten.validate_sensor", return_value=False
): ):
result = await flow.async_step_user(user_input=conf) result2 = await hass.config_entries.flow.async_configure(
assert result["errors"] == {CONF_SENSOR_ID: "invalid_sensor"} result["flow_id"],
user_input={CONF_SENSOR_ID: 12345},
)
assert result2.get("type") == RESULT_TYPE_FORM
assert result2.get("step_id") == SOURCE_USER
assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"}
async def test_show_form(hass): async def test_step_user(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None:
"""Test that the form is served with no input."""
flow = config_flow.LuftDatenFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=None)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
async def test_step_user(hass):
"""Test that the user step works.""" """Test that the user step works."""
conf = { result = await hass.config_entries.flow.async_init(
CONF_SENSOR_ID: "12345abcde", DOMAIN, context={"source": SOURCE_USER}
CONF_SHOW_ON_MAP: False, )
CONF_SCAN_INTERVAL: timedelta(minutes=5),
}
flow = config_flow.LuftDatenFlowHandler() assert result.get("type") == RESULT_TYPE_FORM
flow.hass = hass assert result.get("step_id") == SOURCE_USER
assert "flow_id" in result
with patch("luftdaten.Luftdaten.get_data", return_value=True), patch( with patch("luftdaten.Luftdaten.get_data", return_value=True), patch(
"luftdaten.Luftdaten.validate_sensor", return_value=True "luftdaten.Luftdaten.validate_sensor", return_value=True
): ):
result = await flow.async_step_user(user_input=conf) result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_SENSOR_ID: 12345,
CONF_SHOW_ON_MAP: False,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "12345abcde" assert result2.get("title") == "12345"
assert result["data"] == { assert result2.get("data") == {
CONF_SENSOR_ID: "12345abcde", CONF_SENSOR_ID: 12345,
CONF_SHOW_ON_MAP: False, CONF_SHOW_ON_MAP: False,
CONF_SCAN_INTERVAL: 300, CONF_SCAN_INTERVAL: 600.0,
} }