Add Twente Milieu integration (#25129)

* Adds Twente Milieu integration

* Addresses flake8 warnings

* Adds required test deps

* Fixes path typo in coveragerc

* dispatcher_send -> async_dispatcher_send

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Removes not needed __init__

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Remove explicitly setting None default value on get call

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Correct typo in comment

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Clean storage for only the unloaded entry

Signed-off-by: Franck Nijhof <frenck@addons.community>

* asyncio.wait on updating all integrations

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Use string formatting

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Set a more sane SCAN_INTERVAL

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Small refactor around services

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Small styling correction

* Extract update logic into own function

Signed-off-by: Franck Nijhof <frenck@addons.community>

* Addresses flake8 warnings
This commit is contained in:
Franck Nijhof 2019-07-14 12:30:23 +02:00 committed by Martin Hjelmare
parent 369e6a3905
commit 9d4b5ee58d
16 changed files with 539 additions and 0 deletions

View file

@ -649,6 +649,8 @@ omit =
homeassistant/components/transmission/*
homeassistant/components/travisci/sensor.py
homeassistant/components/tuya/*
homeassistant/components/twentemilieu/const.py
homeassistant/components/twentemilieu/sensor.py
homeassistant/components/twilio_call/notify.py
homeassistant/components/twilio_sms/notify.py
homeassistant/components/twitch/sensor.py

View file

@ -271,6 +271,7 @@ homeassistant/components/traccar/* @ludeeus
homeassistant/components/tradfri/* @ggravlingen
homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/tts/* @robbiet480
homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twilio_call/* @robbiet480
homeassistant/components/twilio_sms/* @robbiet480
homeassistant/components/unifi/* @kane610

View file

@ -0,0 +1,23 @@
{
"config": {
"title": "Twente Milieu",
"step": {
"user": {
"title": "Twente Milieu",
"description": "Set up Twente Milieu providing waste collection information on your address.",
"data": {
"post_code": "Postal code",
"house_number": "House number",
"house_letter": "House letter/additional"
}
}
},
"error": {
"connection_error": "Failed to connect.",
"invalid_address": "Address not found in Twente Milieu service area."
},
"abort": {
"address_already_set_up": "Address already set up."
}
}
}

View file

@ -0,0 +1,103 @@
"""Support for Twente Milieu."""
import asyncio
from datetime import timedelta
import logging
from typing import Optional
from twentemilieu import TwenteMilieu
import voluptuous as vol
from homeassistant.components.twentemilieu.const import (
CONF_HOUSE_LETTER,
CONF_HOUSE_NUMBER,
CONF_POST_CODE,
DATA_UPDATE,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
SCAN_INTERVAL = timedelta(seconds=3600)
_LOGGER = logging.getLogger(__name__)
SERVICE_UPDATE = "update"
SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string})
async def _update_twentemilieu(
hass: HomeAssistantType,
unique_id: Optional[str]
) -> None:
"""Update Twente Milieu."""
if unique_id is not None:
twentemilieu = hass.data[DOMAIN].get(unique_id)
if twentemilieu is not None:
await twentemilieu.update()
async_dispatcher_send(hass, DATA_UPDATE, unique_id)
else:
tasks = []
for twentemilieu in hass.data[DOMAIN].values():
tasks.append(twentemilieu.update())
await asyncio.wait(tasks)
for uid in hass.data[DOMAIN]:
async_dispatcher_send(hass, DATA_UPDATE, uid)
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up the Twente Milieu components."""
async def update(call) -> None:
"""Service call to manually update the data."""
unique_id = call.data.get(CONF_ID)
await _update_twentemilieu(hass, unique_id)
hass.services.async_register(
DOMAIN, SERVICE_UPDATE, update, schema=SERVICE_SCHEMA
)
return True
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry
) -> bool:
"""Set up Twente Milieu from a config entry."""
session = async_get_clientsession(hass)
twentemilieu = TwenteMilieu(
post_code=entry.data[CONF_POST_CODE],
house_number=entry.data[CONF_HOUSE_NUMBER],
house_letter=entry.data[CONF_HOUSE_LETTER],
session=session,
)
unique_id = entry.data[CONF_ID]
hass.data.setdefault(DOMAIN, {})[unique_id] = twentemilieu
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
async def _interval_update(now=None) -> None:
"""Update Twente Milieu data."""
await _update_twentemilieu(hass, unique_id)
async_track_time_interval(hass, _interval_update, SCAN_INTERVAL)
return True
async def async_unload_entry(
hass: HomeAssistantType, entry: ConfigEntry
) -> bool:
"""Unload Twente Milieu config entry."""
await hass.config_entries.async_forward_entry_unload(entry, "sensor")
del hass.data[DOMAIN][entry.data[CONF_ID]]
return True

View file

@ -0,0 +1,84 @@
"""Config flow to configure the Twente Milieu integration."""
import logging
from twentemilieu import (
TwenteMilieu,
TwenteMilieuAddressError,
TwenteMilieuConnectionError,
)
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.twentemilieu.const import (
CONF_HOUSE_LETTER,
CONF_HOUSE_NUMBER,
CONF_POST_CODE,
DOMAIN,
)
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_ID
from homeassistant.helpers.aiohttp_client import async_get_clientsession
_LOGGER = logging.getLogger(__name__)
@config_entries.HANDLERS.register(DOMAIN)
class TwenteMilieuFlowHandler(ConfigFlow):
"""Handle a Twente Milieu config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
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=vol.Schema(
{
vol.Required(CONF_POST_CODE): str,
vol.Required(CONF_HOUSE_NUMBER): str,
vol.Optional(CONF_HOUSE_LETTER): str,
}
),
errors=errors or {},
)
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
if user_input is None:
return await self._show_setup_form(user_input)
errors = {}
session = async_get_clientsession(self.hass)
twentemilieu = TwenteMilieu(
post_code=user_input[CONF_POST_CODE],
house_number=user_input[CONF_HOUSE_NUMBER],
house_letter=user_input.get(CONF_HOUSE_LETTER),
session=session,
)
try:
unique_id = await twentemilieu.unique_id()
except TwenteMilieuConnectionError:
errors["base"] = "connection_error"
return await self._show_setup_form(errors)
except TwenteMilieuAddressError:
errors["base"] = "invalid_address"
return await self._show_setup_form(errors)
entries = self._async_current_entries()
for entry in entries:
if entry.data[CONF_ID] == unique_id:
return self.async_abort(reason="address_already_set_up")
return self.async_create_entry(
title=unique_id,
data={
CONF_ID: unique_id,
CONF_POST_CODE: user_input[CONF_POST_CODE],
CONF_HOUSE_NUMBER: user_input[CONF_HOUSE_NUMBER],
CONF_HOUSE_LETTER: user_input.get(CONF_HOUSE_LETTER),
},
)

View file

@ -0,0 +1,9 @@
"""Constants for the Twente Milieu integration."""
DOMAIN = "twentemilieu"
DATA_UPDATE = "twentemilieu_update"
CONF_POST_CODE = "post_code"
CONF_HOUSE_NUMBER = "house_number"
CONF_HOUSE_LETTER = "house_letter"

View file

@ -0,0 +1,13 @@
{
"domain": "twentemilieu",
"name": "Twente Milieu",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/twentemilieu",
"requirements": [
"twentemilieu==0.1.0"
],
"dependencies": [],
"codeowners": [
"@frenck"
]
}

View file

@ -0,0 +1,154 @@
"""Support for Twente Milieu sensors."""
import logging
from typing import Any, Dict
from twentemilieu import (
WASTE_TYPE_NON_RECYCLABLE,
WASTE_TYPE_ORGANIC,
WASTE_TYPE_PAPER,
WASTE_TYPE_PLASTIC,
TwenteMilieu,
TwenteMilieuConnectionError,
)
from homeassistant.components.twentemilieu.const import DATA_UPDATE, DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up Twente Milieu sensor based on a config entry."""
twentemilieu = hass.data[DOMAIN][entry.data[CONF_ID]]
try:
await twentemilieu.update()
except TwenteMilieuConnectionError as exception:
raise PlatformNotReady from exception
sensors = [
TwenteMilieuSensor(
twentemilieu,
unique_id=entry.data[CONF_ID],
name="{} Waste Pickup".format(WASTE_TYPE_NON_RECYCLABLE),
waste_type=WASTE_TYPE_NON_RECYCLABLE,
icon="mdi:delete-empty",
),
TwenteMilieuSensor(
twentemilieu,
unique_id=entry.data[CONF_ID],
name="{} Waste Pickup".format(WASTE_TYPE_ORGANIC),
waste_type=WASTE_TYPE_ORGANIC,
icon="mdi:delete-empty",
),
TwenteMilieuSensor(
twentemilieu,
unique_id=entry.data[CONF_ID],
name="{} Waste Pickup".format(WASTE_TYPE_PAPER),
waste_type=WASTE_TYPE_PAPER,
icon="mdi:delete-empty",
),
TwenteMilieuSensor(
twentemilieu,
unique_id=entry.data[CONF_ID],
name="{} Waste Pickup".format(WASTE_TYPE_PLASTIC),
waste_type=WASTE_TYPE_PLASTIC,
icon="mdi:delete-empty",
),
]
async_add_entities(sensors, True)
class TwenteMilieuSensor(Entity):
"""Defines a Twente Milieu sensor."""
def __init__(
self,
twentemilieu: TwenteMilieu,
unique_id: str,
name: str,
waste_type: str,
icon: str,
) -> None:
"""Initialize the Twente Milieu entity."""
self._available = True
self._unique_id = unique_id
self._icon = icon
self._name = name
self._twentemilieu = twentemilieu
self._waste_type = waste_type
self._unsub_dispatcher = None
self._state = None
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def icon(self) -> str:
"""Return the mdi icon of the entity."""
return self._icon
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
@property
def unique_id(self) -> str:
"""Return the unique ID for this sensor."""
return "{}_{}_{}".format(DOMAIN, self._unique_id, self._waste_type)
@property
def should_poll(self) -> bool:
"""Return the polling requirement of the entity."""
return False
async def async_added_to_hass(self) -> None:
"""Connect to dispatcher listening for entity data notifications."""
self._unsub_dispatcher = async_dispatcher_connect(
self.hass, DATA_UPDATE, self._schedule_immediate_update
)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect from update signal."""
self._unsub_dispatcher()
@callback
def _schedule_immediate_update(self, unique_id: str) -> None:
"""Schedule an immediate update of the entity."""
if unique_id == self._unique_id:
self.async_schedule_update_ha_state(True)
@property
def state(self):
"""Return the state of the sensor."""
return self._state
async def async_update(self) -> None:
"""Update Twente Milieu entity."""
next_pickup = await self._twentemilieu.next_pickup(self._waste_type)
if next_pickup is not None:
self._state = next_pickup.date().isoformat()
@property
def device_info(self) -> Dict[str, Any]:
"""Return device information about Twente Milieu."""
return {
"identifiers": {(DOMAIN, self._unique_id)},
"name": "Twente Milieu",
"manufacturer": "Twente Milieu",
}

View file

@ -0,0 +1,6 @@
update:
description: Update all entities with fresh data from Twente Milieu
fields:
id:
description: Specific unique address ID to update
example: 1300012345

View file

@ -0,0 +1,23 @@
{
"config": {
"title": "Twente Milieu",
"step": {
"user": {
"title": "Twente Milieu",
"description": "Set up Twente Milieu providing waste collection information on your address.",
"data": {
"post_code": "Postal code",
"house_number": "House number",
"house_letter": "House letter/additional"
}
}
},
"error": {
"connection_error": "Failed to connect.",
"invalid_address": "Address not found in Twente Milieu service area."
},
"abort": {
"address_exists": "Address already set up."
}
}
}

View file

@ -52,6 +52,7 @@ FLOWS = [
"toon",
"tplink",
"tradfri",
"twentemilieu",
"twilio",
"unifi",
"upnp",

View file

@ -1830,6 +1830,9 @@ transmissionrpc==0.11
# homeassistant.components.tuya
tuyaha==0.0.2
# homeassistant.components.twentemilieu
twentemilieu==0.1.0
# homeassistant.components.twilio
twilio==6.19.1

View file

@ -358,6 +358,9 @@ statsd==3.2.1
# homeassistant.components.toon
toonapilib==3.2.4
# homeassistant.components.twentemilieu
twentemilieu==0.1.0
# homeassistant.components.uvc
uvcclient==0.11.0

View file

@ -148,6 +148,7 @@ TEST_REQUIREMENTS = (
'srpenergy',
'statsd',
'toonapilib',
'twentemilieu',
'uvcclient',
'vsure',
'warrant',

View file

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

View file

@ -0,0 +1,112 @@
"""Tests for the Twente Milieu config flow."""
import aiohttp
from homeassistant import data_entry_flow
from homeassistant.components.twentemilieu import config_flow
from homeassistant.components.twentemilieu.const import (
CONF_HOUSE_LETTER,
CONF_HOUSE_NUMBER,
CONF_POST_CODE,
DOMAIN,
)
from homeassistant.const import CONF_ID
from tests.common import MockConfigEntry
FIXTURE_USER_INPUT = {
CONF_ID: "12345",
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
CONF_HOUSE_LETTER: "A",
}
async def test_show_set_form(hass):
"""Test that the setup form is served."""
flow = config_flow.TwenteMilieuFlowHandler()
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_connection_error(hass, aioclient_mock):
"""Test we show user form on Twente Milieu connection error."""
aioclient_mock.post(
"https://wasteapi.2go-mobile.com/api/FetchAdress",
exc=aiohttp.ClientError,
)
flow = config_flow.TwenteMilieuFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "connection_error"}
async def test_invalid_address(hass, aioclient_mock):
"""Test we show user form on Twente Milieu invalid address error."""
aioclient_mock.post(
"https://wasteapi.2go-mobile.com/api/FetchAdress",
json={"dataList": []},
headers={"Content-Type": "application/json"},
)
flow = config_flow.TwenteMilieuFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "invalid_address"}
async def test_address_already_set_up(hass, aioclient_mock):
"""Test we abort if address has already been set up."""
MockConfigEntry(
domain=DOMAIN, data=FIXTURE_USER_INPUT, title="12345"
).add_to_hass(hass)
aioclient_mock.post(
"https://wasteapi.2go-mobile.com/api/FetchAdress",
json={"dataList": [{"UniqueId": "12345"}]},
headers={"Content-Type": "application/json"},
)
flow = config_flow.TwenteMilieuFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "address_already_set_up"
async def test_full_flow_implementation(hass, aioclient_mock):
"""Test registering an integration and finishing flow works."""
aioclient_mock.post(
"https://wasteapi.2go-mobile.com/api/FetchAdress",
json={"dataList": [{"UniqueId": "12345"}]},
headers={"Content-Type": "application/json"},
)
flow = config_flow.TwenteMilieuFlowHandler()
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"
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "12345"
assert result["data"][CONF_POST_CODE] == FIXTURE_USER_INPUT[CONF_POST_CODE]
assert (
result["data"][CONF_HOUSE_NUMBER]
== FIXTURE_USER_INPUT[CONF_HOUSE_NUMBER]
)
assert (
result["data"][CONF_HOUSE_LETTER]
== FIXTURE_USER_INPUT[CONF_HOUSE_LETTER]
)