Add Avri config flow (#34288)

* Add config flow to Avri integration

* Add config flow validation

* Update .coveragerc

* Start adding config flow tests

* Fix failing test

* Fix pylint

* Update homeassistant/components/avri/config_flow.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/avri/config_flow.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Fix import order

* Code review comments

* Update homeassistant/components/avri/sensor.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Remove device information

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Tim van Cann 2020-06-06 16:37:31 +02:00 committed by GitHub
parent 14f5cab71d
commit d73a4e1ed5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 344 additions and 47 deletions

View file

@ -68,6 +68,7 @@ omit =
homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/aurora_abb_powerone/sensor.py
homeassistant/components/avea/light.py homeassistant/components/avea/light.py
homeassistant/components/avion/light.py homeassistant/components/avion/light.py
homeassistant/components/avri/const.py
homeassistant/components/avri/sensor.py homeassistant/components/avri/sensor.py
homeassistant/components/azure_event_hub/* homeassistant/components/azure_event_hub/*
homeassistant/components/azure_service_bus/* homeassistant/components/azure_service_bus/*

View file

@ -0,0 +1,24 @@
{
"config": {
"abort": {
"already_configured": "This address is already configured."
},
"error": {
"invalid_country_code": "Unknown 2 letter country code.",
"invalid_house_number": "Invalid house number."
},
"step": {
"user": {
"data": {
"country_code": "2 Letter country code",
"house_number": "House number",
"house_number_extension": "House number extension",
"zip_code": "Zip code"
},
"description": "Enter your address",
"title": "Avri"
}
}
},
"title": "Avri"
}

View file

@ -0,0 +1,24 @@
{
"config": {
"abort": {
"already_configured": "Dit adres is reeds geconfigureerd."
},
"error": {
"invalid_country_code": "Onbekende landcode",
"invalid_house_number": "Ongeldig huisnummer."
},
"step": {
"user": {
"data": {
"country_code": "2 Letter landcode",
"house_number": "Huisnummer",
"house_number_extension": "Huisnummer toevoeging",
"zip_code": "Postcode"
},
"description": "Vul je adres in.",
"title": "Avri"
}
}
},
"title": "Avri"
}

View file

@ -1 +1,63 @@
"""The avri component.""" """The avri component."""
import asyncio
from datetime import timedelta
import logging
from avri.api import Avri
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import (
CONF_COUNTRY_CODE,
CONF_HOUSE_NUMBER,
CONF_HOUSE_NUMBER_EXTENSION,
CONF_ZIP_CODE,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
SCAN_INTERVAL = timedelta(hours=4)
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Avri component."""
hass.data[DOMAIN] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Avri from a config entry."""
client = Avri(
postal_code=entry.data[CONF_ZIP_CODE],
house_nr=entry.data[CONF_HOUSE_NUMBER],
house_nr_extension=entry.data.get(CONF_HOUSE_NUMBER_EXTENSION),
country_code=entry.data[CONF_COUNTRY_CODE],
)
hass.data[DOMAIN][entry.entry_id] = client
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View file

@ -0,0 +1,74 @@
"""Config flow for Avri component."""
import pycountry
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_ID
from .const import (
CONF_COUNTRY_CODE,
CONF_HOUSE_NUMBER,
CONF_HOUSE_NUMBER_EXTENSION,
CONF_ZIP_CODE,
DEFAULT_COUNTRY_CODE,
)
from .const import DOMAIN # pylint:disable=unused-import
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_ZIP_CODE): str,
vol.Required(CONF_HOUSE_NUMBER): int,
vol.Optional(CONF_HOUSE_NUMBER_EXTENSION): str,
vol.Optional(CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY_CODE): str,
}
)
class AvriConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Avri config flow."""
VERSION = 1
async def _show_setup_form(self, errors=None):
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors or {},
)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if user_input is None:
return await self._show_setup_form()
zip_code = user_input[CONF_ZIP_CODE].replace(" ", "").upper()
errors = {}
if user_input[CONF_HOUSE_NUMBER] <= 0:
errors[CONF_HOUSE_NUMBER] = "invalid_house_number"
return await self._show_setup_form(errors)
if not pycountry.countries.get(alpha_2=user_input[CONF_COUNTRY_CODE]):
errors[CONF_COUNTRY_CODE] = "invalid_country_code"
return await self._show_setup_form(errors)
unique_id = (
f"{zip_code}"
f" "
f"{user_input[CONF_HOUSE_NUMBER]}"
f'{user_input.get(CONF_HOUSE_NUMBER_EXTENSION, "")}'
)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=unique_id,
data={
CONF_ID: unique_id,
CONF_ZIP_CODE: zip_code,
CONF_HOUSE_NUMBER: user_input[CONF_HOUSE_NUMBER],
CONF_HOUSE_NUMBER_EXTENSION: user_input.get(
CONF_HOUSE_NUMBER_EXTENSION, ""
),
CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE],
},
)

View file

@ -0,0 +1,8 @@
"""Constants for the Avri integration."""
CONF_COUNTRY_CODE = "country_code"
CONF_ZIP_CODE = "zip_code"
CONF_HOUSE_NUMBER = "house_number"
CONF_HOUSE_NUMBER_EXTENSION = "house_number_extension"
DOMAIN = "avri"
ICON = "mdi:trash-can-outline"
DEFAULT_COUNTRY_CODE = "NL"

View file

@ -2,6 +2,12 @@
"domain": "avri", "domain": "avri",
"name": "Avri", "name": "Avri",
"documentation": "https://www.home-assistant.io/integrations/avri", "documentation": "https://www.home-assistant.io/integrations/avri",
"requirements": ["avri-api==0.1.7"], "requirements": [
"codeowners": ["@timvancann"] "avri-api==0.1.7",
} "pycountry==19.8.18"
],
"codeowners": [
"@timvancann"
],
"config_flow": true
}

View file

@ -1,45 +1,25 @@
"""Support for Avri waste curbside collection pickup.""" """Support for Avri waste curbside collection pickup."""
from datetime import timedelta
import logging import logging
from avri.api import Avri, AvriException from avri.api import Avri, AvriException
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_ID, DEVICE_CLASS_TIMESTAMP
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN, ICON
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_COUNTRY_CODE = "country_code"
CONF_ZIP_CODE = "zip_code"
CONF_HOUSE_NUMBER = "house_number"
CONF_HOUSE_NUMBER_EXTENSION = "house_number_extension"
DEFAULT_NAME = "avri"
ICON = "mdi:trash-can-outline"
SCAN_INTERVAL = timedelta(hours=4)
DEFAULT_COUNTRY_CODE = "NL"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ZIP_CODE): cv.string,
vol.Required(CONF_HOUSE_NUMBER): cv.positive_int,
vol.Optional(CONF_HOUSE_NUMBER_EXTENSION): cv.string,
vol.Optional(CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY_CODE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the Avri Waste platform.""" """Set up the Avri Waste platform."""
client = Avri( client = hass.data[DOMAIN][entry.entry_id]
postal_code=config[CONF_ZIP_CODE], integration_id = entry.data[CONF_ID]
house_nr=config[CONF_HOUSE_NUMBER],
house_nr_extension=config.get(CONF_HOUSE_NUMBER_EXTENSION),
country_code=config[CONF_COUNTRY_CODE],
)
try: try:
each_upcoming = client.upcoming_of_each() each_upcoming = client.upcoming_of_each()
@ -47,22 +27,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
raise PlatformNotReady from ex raise PlatformNotReady from ex
else: else:
entities = [ entities = [
AvriWasteUpcoming(config[CONF_NAME], client, upcoming.name) AvriWasteUpcoming(client, upcoming.name, integration_id)
for upcoming in each_upcoming for upcoming in each_upcoming
] ]
add_entities(entities, True) async_add_entities(entities, True)
class AvriWasteUpcoming(Entity): class AvriWasteUpcoming(Entity):
"""Avri Waste Sensor.""" """Avri Waste Sensor."""
def __init__(self, name: str, client: Avri, waste_type: str): def __init__(self, client: Avri, waste_type: str, integration_id: str):
"""Initialize the sensor.""" """Initialize the sensor."""
self._waste_type = waste_type self._waste_type = waste_type
self._name = f"{name}_{self._waste_type}" self._name = f"{self._waste_type}".title()
self._state = None self._state = None
self._client = client self._client = client
self._state_available = False self._state_available = False
self._integration_id = integration_id
@property @property
def name(self): def name(self):
@ -72,13 +53,7 @@ class AvriWasteUpcoming(Entity):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return a unique ID.""" """Return a unique ID."""
return ( return (f"{self._integration_id}" f"-{self._waste_type}").replace(" ", "")
f"{self._waste_type}"
f"-{self._client.country_code}"
f"-{self._client.postal_code}"
f"-{self._client.house_nr}"
f"-{self._client.house_nr_extension}"
)
@property @property
def state(self): def state(self):
@ -90,13 +65,21 @@ class AvriWasteUpcoming(Entity):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._state_available return self._state_available
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_TIMESTAMP
@property @property
def icon(self): def icon(self):
"""Icon to use in the frontend.""" """Icon to use in the frontend."""
return ICON return ICON
def update(self): async def async_update(self):
"""Update device state.""" """Update the data."""
if not self.enabled:
return
try: try:
pickup_events = self._client.upcoming_of_each() pickup_events = self._client.upcoming_of_each()
except AvriException as ex: except AvriException as ex:

View file

@ -0,0 +1,24 @@
{
"title": "Avri",
"config": {
"abort": {
"already_configured": "This address is already configured."
},
"error": {
"invalid_house_number": "Invalid house number.",
"invalid_country_code": "Unknown 2 letter country code."
},
"step": {
"user": {
"data": {
"zip_code": "Zip code",
"house_number": "House number",
"house_number_extension": "House number extension",
"country_code": "2 Letter country code"
},
"description": "Enter your address",
"title": "Avri"
}
}
}
}

View file

@ -17,6 +17,7 @@ FLOWS = [
"ambient_station", "ambient_station",
"atag", "atag",
"august", "august",
"avri",
"axis", "axis",
"blebox", "blebox",
"blink", "blink",

View file

@ -1259,6 +1259,9 @@ pycomfoconnect==0.3
# homeassistant.components.coolmaster # homeassistant.components.coolmaster
pycoolmasternet==0.0.4 pycoolmasternet==0.0.4
# homeassistant.components.avri
pycountry==19.8.18
# homeassistant.components.microsoft # homeassistant.components.microsoft
pycsspeechtts==1.0.3 pycsspeechtts==1.0.3

View file

@ -146,6 +146,9 @@ async-upnp-client==0.14.13
# homeassistant.components.stream # homeassistant.components.stream
av==8.0.2 av==8.0.2
# homeassistant.components.avri
avri-api==0.1.7
# homeassistant.components.axis # homeassistant.components.axis
axis==29 axis==29
@ -544,6 +547,9 @@ pychromecast==6.0.0
# homeassistant.components.coolmaster # homeassistant.components.coolmaster
pycoolmasternet==0.0.4 pycoolmasternet==0.0.4
# homeassistant.components.avri
pycountry==19.8.18
# homeassistant.components.daikin # homeassistant.components.daikin
pydaikin==2.1.1 pydaikin==2.1.1

View file

@ -8,4 +8,4 @@ cd "$(dirname "$0")/.."
script/bootstrap script/bootstrap
pre-commit install pre-commit install
pip3 install -e . pip install -e .

View file

@ -0,0 +1 @@
"""Tests for the Avri integration."""

View file

@ -0,0 +1,80 @@
"""Test the Avri config flow."""
from asynctest import patch
from homeassistant import config_entries, setup
from homeassistant.components.avri.const import DOMAIN
async def test_form(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "avri", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.avri.async_setup_entry", return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"zip_code": "1234AB",
"house_number": 42,
"house_number_extension": "",
"country_code": "NL",
},
)
assert result2["type"] == "create_entry"
assert result2["title"] == "1234AB 42"
assert result2["data"] == {
"id": "1234AB 42",
"zip_code": "1234AB",
"house_number": 42,
"house_number_extension": "",
"country_code": "NL",
}
await hass.async_block_till_done()
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_house_number(hass):
"""Test we handle invalid house number."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"zip_code": "1234AB",
"house_number": -1,
"house_number_extension": "",
"country_code": "NL",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"house_number": "invalid_house_number"}
async def test_form_invalid_country_code(hass):
"""Test we handle invalid county code."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"zip_code": "1234AB",
"house_number": 42,
"house_number_extension": "",
"country_code": "foo",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"country_code": "invalid_country_code"}