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/avea/light.py
homeassistant/components/avion/light.py
homeassistant/components/avri/const.py
homeassistant/components/avri/sensor.py
homeassistant/components/azure_event_hub/*
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."""
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",
"name": "Avri",
"documentation": "https://www.home-assistant.io/integrations/avri",
"requirements": ["avri-api==0.1.7"],
"codeowners": ["@timvancann"]
}
"requirements": [
"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."""
from datetime import timedelta
import logging
from avri.api import Avri, AvriException
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, DEVICE_CLASS_TIMESTAMP
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN, ICON
_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."""
client = Avri(
postal_code=config[CONF_ZIP_CODE],
house_nr=config[CONF_HOUSE_NUMBER],
house_nr_extension=config.get(CONF_HOUSE_NUMBER_EXTENSION),
country_code=config[CONF_COUNTRY_CODE],
)
client = hass.data[DOMAIN][entry.entry_id]
integration_id = entry.data[CONF_ID]
try:
each_upcoming = client.upcoming_of_each()
@ -47,22 +27,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
raise PlatformNotReady from ex
else:
entities = [
AvriWasteUpcoming(config[CONF_NAME], client, upcoming.name)
AvriWasteUpcoming(client, upcoming.name, integration_id)
for upcoming in each_upcoming
]
add_entities(entities, True)
async_add_entities(entities, True)
class AvriWasteUpcoming(Entity):
"""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."""
self._waste_type = waste_type
self._name = f"{name}_{self._waste_type}"
self._name = f"{self._waste_type}".title()
self._state = None
self._client = client
self._state_available = False
self._integration_id = integration_id
@property
def name(self):
@ -72,13 +53,7 @@ class AvriWasteUpcoming(Entity):
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return (
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}"
)
return (f"{self._integration_id}" f"-{self._waste_type}").replace(" ", "")
@property
def state(self):
@ -90,13 +65,21 @@ class AvriWasteUpcoming(Entity):
"""Return True if entity is available."""
return self._state_available
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_TIMESTAMP
@property
def icon(self):
"""Icon to use in the frontend."""
return ICON
def update(self):
"""Update device state."""
async def async_update(self):
"""Update the data."""
if not self.enabled:
return
try:
pickup_events = self._client.upcoming_of_each()
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",
"atag",
"august",
"avri",
"axis",
"blebox",
"blink",

View File

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

View File

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

View File

@ -8,4 +8,4 @@ cd "$(dirname "$0")/.."
script/bootstrap
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"}