Support for Point component (#17466)

* Minut Point support

* tox tests for Point

* config flow fixes

* fixes requested by @MartinHjelmare

* swedish translation :)

* fix tests
This commit is contained in:
Fredrik Erlandsson 2018-11-19 12:52:21 +01:00 committed by Paulus Schoutsen
parent 84fd66c8a1
commit c1ca7beea1
13 changed files with 935 additions and 0 deletions

View file

@ -262,6 +262,10 @@ omit =
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
homeassistant/components/point/__init__.py
homeassistant/components/point/const.py
homeassistant/components/*/point.py
homeassistant/components/switch/qwikswitch.py
homeassistant/components/light/qwikswitch.py

View file

@ -0,0 +1,103 @@
"""
Support for Minut Point.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.point/
"""
import logging
from homeassistant.components.point import MinutPointEntity
from homeassistant.components.point.const import (
DOMAIN as POINT_DOMAIN, NEW_DEVICE, SIGNAL_WEBHOOK)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
EVENTS = {
'battery': # On means low, Off means normal
('battery_low', ''),
'button_press': # On means the button was pressed, Off means normal
('short_button_press', ''),
'cold': # On means cold, Off means normal
('temperature_low', 'temperature_risen_normal'),
'connectivity': # On means connected, Off means disconnected
('device_online', 'device_offline'),
'dry': # On means too dry, Off means normal
('humidity_low', 'humidity_risen_normal'),
'heat': # On means hot, Off means normal
('temperature_high', 'temperature_dropped_normal'),
'moisture': # On means wet, Off means dry
('humidity_high', 'humidity_dropped_normal'),
'sound': # On means sound detected, Off means no sound (clear)
('avg_sound_high', 'sound_level_dropped_normal'),
'tamper': # On means the point was removed or attached
('tamper', ''),
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Point's binary sensors based on a config entry."""
device_id = config_entry.data[NEW_DEVICE]
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
async_add_entities((MinutPointBinarySensor(client, device_id, device_class)
for device_class in EVENTS), True)
class MinutPointBinarySensor(MinutPointEntity):
"""The platform class required by Home Assistant."""
def __init__(self, point_client, device_id, device_class):
"""Initialize the entity."""
super().__init__(point_client, device_id, device_class)
self._async_unsub_hook_dispatcher_connect = None
self._events = EVENTS[device_class]
self._is_on = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
await super().async_added_to_hass()
self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect(
self.hass, SIGNAL_WEBHOOK, self._webhook_event)
async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""
await super().async_will_remove_from_hass()
if self._async_unsub_hook_dispatcher_connect:
self._async_unsub_hook_dispatcher_connect()
@callback
def _update_callback(self):
"""Update the value of the sensor."""
if not self.is_updated:
return
if self._events[0] in self.device.ongoing_events:
self._is_on = True
else:
self._is_on = None
self.async_schedule_update_ha_state()
@callback
def _webhook_event(self, data, webhook):
"""Process new event from the webhook."""
if self.device.webhook != webhook:
return
_type = data.get('event', {}).get('type')
if _type not in self._events:
return
_LOGGER.debug("Recieved webhook: %s", _type)
if _type == self._events[0]:
self._is_on = True
if _type == self._events[1]:
self._is_on = None
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Return the state of the binary sensor."""
if self.device_class == 'connectivity':
# connectivity is the other way around.
return not self._is_on
return self._is_on

View file

@ -0,0 +1,33 @@
{
"config": {
"title": "Minut Point",
"step": {
"user": {
"title": "Authentication Provider",
"description": "Pick via which authentication provider you want to authenticate with Point.",
"data": {
"flow_impl": "Provider"
}
},
"auth": {
"title": "Authenticate Point",
"description": "Please follow the link below and <b>Accept</b> access to your Minut account, then come back and press <b>Submit</b> below.\n\n[Link]({authorization_url})"
}
},
"create_entry": {
"default": "Successfully authenticated with Minut for your Point device(s)"
},
"error": {
"no_token": "Not authenticated with Minut",
"follow_link": "Please follow the link and authenticate before pressing Submit"
},
"abort": {
"already_setup": "You can only configure a Point account.",
"external_setup": "Point successfully configured from another flow.",
"no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/).",
"authorize_url_timeout": "Timeout generating authorize url.",
"authorize_url_fail": "Unknown error generating an authorize url."
}
}
}

View file

@ -0,0 +1,33 @@
{
"config": {
"title": "Minut Point",
"step": {
"user": {
"title": "Autentiseringsleverant\u00f6r",
"description": "V\u00e4lj den autentiseringsleverant\u00f6r som du vill autentisera med mot Point.",
"data": {
"flow_impl": "Leverant\u00f6r"
}
},
"auth": {
"title": "Autentisera Point",
"description": "F\u00f6lj l\u00e4nken nedan och klicka p\u00e5 <b>Accept</b> f\u00f6r att tilll\u00e5ta tillg\u00e5ng till ditt Minut konto, kom d\u00f6refter tillbaka hit och kicka p\u00e5 <b>Submit</b> nedan.\n\n[L\u00e4nk]({authorization_url})"
}
},
"create_entry": {
"default": "Autentiserad med Minut f\u00f6r era Point enheter."
},
"error": {
"no_token": "Inte autentiserad hos Minut",
"follow_link": "F\u00f6lj l\u00e4nken och autentisera innan du kickar på Submit"
},
"abort": {
"already_setup": "Du kan endast konfigurera ett Point-konto.",
"external_setup": "Point har lyckats konfigureras fr\u00e5n ett annat fl\u00f6de.",
"no_flows": "Du m\u00e5ste konfigurera Nest innan du kan autentisera med det. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/point/).",
"authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.",
"authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress."
}
}
}

View file

@ -0,0 +1,306 @@
"""
Support for Minut Point.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/point/
"""
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp
from . import config_flow # noqa pylint_disable=unused-import
from .const import (
CONF_WEBHOOK_URL, DOMAIN, EVENT_RECEIVED, NEW_DEVICE, SCAN_INTERVAL,
SIGNAL_UPDATE_ENTITY, SIGNAL_WEBHOOK)
REQUIREMENTS = ['pypoint==1.0.5']
DEPENDENCIES = ['webhook']
_LOGGER = logging.getLogger(__name__)
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN:
vol.Schema({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
})
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up the Minut Point component."""
if DOMAIN not in config:
return True
conf = config[DOMAIN]
config_flow.register_flow_implementation(
hass, DOMAIN, conf[CONF_CLIENT_ID],
conf[CONF_CLIENT_SECRET])
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': config_entries.SOURCE_IMPORT},
))
return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Set up Point from a config entry."""
from pypoint import PointSession
def token_saver(token):
_LOGGER.debug('Saving updated token')
entry.data[CONF_TOKEN] = token
hass.config_entries.async_update_entry(entry, data={**entry.data})
# Force token update.
entry.data[CONF_TOKEN]['expires_in'] = -1
session = PointSession(
entry.data['refresh_args']['client_id'],
token=entry.data[CONF_TOKEN],
auto_refresh_kwargs=entry.data['refresh_args'],
token_saver=token_saver,
)
if not session.is_authorized:
_LOGGER.error('Authentication Error')
return False
await async_setup_webhook(hass, entry, session)
client = MinutPointClient(hass, entry, session)
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client})
await client.update()
return True
async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry,
session):
"""Set up a webhook to handle binary sensor events."""
if CONF_WEBHOOK_ID not in entry.data:
entry.data[CONF_WEBHOOK_ID] = \
hass.components.webhook.async_generate_id()
entry.data[CONF_WEBHOOK_URL] = \
hass.components.webhook.async_generate_url(
entry.data[CONF_WEBHOOK_ID])
_LOGGER.info('Registering new webhook at: %s',
entry.data[CONF_WEBHOOK_URL])
hass.config_entries.async_update_entry(
entry, data={
**entry.data,
})
session.update_webhook(entry.data[CONF_WEBHOOK_URL],
entry.data[CONF_WEBHOOK_ID])
hass.components.webhook.async_register(entry.data[CONF_WEBHOOK_ID],
handle_webhook)
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Unload a config entry."""
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
client = hass.data[DOMAIN].pop(entry.entry_id)
client.remove_webhook()
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
for component in ('binary_sensor', 'sensor'):
await hass.config_entries.async_forward_entry_unload(
entry, component)
return True
async def handle_webhook(hass, webhook_id, request):
"""Handle webhook callback."""
try:
data = await request.json()
_LOGGER.debug("Webhook %s: %s", webhook_id, data)
except ValueError:
return None
if isinstance(data, dict):
data['webhook_id'] = webhook_id
async_dispatcher_send(hass, SIGNAL_WEBHOOK, data, data.get('hook_id'))
hass.bus.async_fire(EVENT_RECEIVED, data)
class MinutPointClient():
"""Get the latest data and update the states."""
def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry,
session):
"""Initialize the Minut data object."""
self._known_devices = []
self._hass = hass
self._config_entry = config_entry
self._is_available = True
self._client = session
async_track_time_interval(self._hass, self.update, SCAN_INTERVAL)
async def update(self, *args):
"""Periodically poll the cloud for current state."""
await self._sync()
async def _sync(self):
"""Update local list of devices."""
if not self._client.update() and self._is_available:
self._is_available = False
_LOGGER.warning("Device is unavailable")
return
self._is_available = True
for device in self._client.devices:
if device.device_id not in self._known_devices:
# A way to communicate the device_id to entry_setup,
# can this be done nicer?
self._config_entry.data[NEW_DEVICE] = device.device_id
await self._hass.config_entries.async_forward_entry_setup(
self._config_entry, 'sensor')
await self._hass.config_entries.async_forward_entry_setup(
self._config_entry, 'binary_sensor')
self._known_devices.append(device.device_id)
del self._config_entry.data[NEW_DEVICE]
async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY)
def device(self, device_id):
"""Return device representation."""
return self._client.device(device_id)
def is_available(self, device_id):
"""Return device availability."""
return device_id in self._client.device_ids
def remove_webhook(self):
"""Remove the session webhook."""
return self._client.remove_webhook()
class MinutPointEntity(Entity):
"""Base Entity used by the sensors."""
def __init__(self, point_client, device_id, device_class):
"""Initialize the entity."""
self._async_unsub_dispatcher_connect = None
self._client = point_client
self._id = device_id
self._name = self.device.name
self._device_class = device_class
self._updated = utc_from_timestamp(0)
self._value = None
def __str__(self):
"""Return string representation of device."""
return "MinutPoint {}".format(self.name)
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
_LOGGER.debug('Created device %s', self)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback)
self._update_callback()
async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
@callback
def _update_callback(self):
"""Update the value of the sensor."""
pass
@property
def available(self):
"""Return true if device is not offline."""
return self._client.is_available(self.device_id)
@property
def device(self):
"""Return the representation of the device."""
return self._client.device(self.device_id)
@property
def device_class(self):
"""Return the device class."""
return self._device_class
@property
def device_id(self):
"""Return the id of the device."""
return self._id
@property
def device_state_attributes(self):
"""Return status of device."""
attrs = self.device.device_status
attrs['last_heard_from'] = \
as_local(self.last_update).strftime("%Y-%m-%d %H:%M:%S")
return attrs
@property
def device_info(self):
"""Return a device description for device registry."""
device = self.device.device
return {
'connections': {('mac', device['device_mac'])},
'identifieres': device['device_id'],
'manufacturer': 'Minut',
'model': 'Point v{}'.format(device['hardware_version']),
'name': device['description'],
'sw_version': device['firmware']['installed'],
}
@property
def name(self):
"""Return the display name of this device."""
return "{} {}".format(self._name, self.device_class.capitalize())
@property
def is_updated(self):
"""Return true if sensor have been updated."""
return self.last_update > self._updated
@property
def last_update(self):
"""Return the last_update time for the device."""
last_update = parse_datetime(self.device.last_update)
return last_update
@property
def should_poll(self):
"""No polling needed for point."""
return False
@property
def unique_id(self):
"""Return the unique id of the sensor."""
return 'point.{}-{}'.format(self._id, self.device_class)
@property
def value(self):
"""Return the sensor value."""
return self._value

View file

@ -0,0 +1,189 @@
"""Config flow for Minut Point."""
import asyncio
from collections import OrderedDict
import logging
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback
from .const import CLIENT_ID, CLIENT_SECRET, DOMAIN
AUTH_CALLBACK_PATH = '/api/minut'
AUTH_CALLBACK_NAME = 'api:minut'
DATA_FLOW_IMPL = 'point_flow_implementation'
_LOGGER = logging.getLogger(__name__)
@callback
def register_flow_implementation(hass, domain, client_id, client_secret):
"""Register a flow implementation.
domain: Domain of the component responsible for the implementation.
name: Name of the component.
client_id: Client id.
client_secret: Client secret.
"""
if DATA_FLOW_IMPL not in hass.data:
hass.data[DATA_FLOW_IMPL] = OrderedDict()
hass.data[DATA_FLOW_IMPL][domain] = {
CLIENT_ID: client_id,
CLIENT_SECRET: client_secret,
}
@config_entries.HANDLERS.register('point')
class PointFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
VERSION = 1
CONNETION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize flow."""
self.flow_impl = None
async def async_step_import(self, user_input=None):
"""Handle external yaml configuration."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
self.flow_impl = DOMAIN
return await self.async_step_auth()
async def async_step_user(self, user_input=None):
"""Handle a flow start."""
flows = self.hass.data.get(DATA_FLOW_IMPL, {})
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
if not flows:
_LOGGER.debug("no flows")
return self.async_abort(reason='no_flows')
if len(flows) == 1:
self.flow_impl = list(flows)[0]
return await self.async_step_auth()
if user_input is not None:
self.flow_impl = user_input['flow_impl']
return await self.async_step_auth()
return self.async_show_form(
step_id='user',
data_schema=vol.Schema({
vol.Required('flow_impl'):
vol.In(list(flows))
}))
async def async_step_auth(self, user_input=None):
"""Create an entry for auth."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='external_setup')
errors = {}
if user_input is not None:
errors['base'] = 'follow_link'
try:
with async_timeout.timeout(10):
url = await self._get_authorization_url()
except asyncio.TimeoutError:
return self.async_abort(reason='authorize_url_timeout')
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error generating auth url")
return self.async_abort(reason='authorize_url_fail')
return self.async_show_form(
step_id='auth',
description_placeholders={'authorization_url': url},
errors=errors,
)
async def _get_authorization_url(self):
"""Create Minut Point session and get authorization url."""
from pypoint import PointSession
flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl]
client_id = flow[CLIENT_ID]
client_secret = flow[CLIENT_SECRET]
point_session = PointSession(
client_id, client_secret=client_secret)
self.hass.http.register_view(MinutAuthCallbackView())
return point_session.get_authorization_url
async def async_step_code(self, code=None):
"""Received code for authentication."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
if code is None:
return self.async_abort(reason='no_code')
_LOGGER.debug("Should close all flows below %s",
self.hass.config_entries.flow.async_progress())
# Remove notification if no other discovery config entries in progress
return await self._async_create_session(code)
async def _async_create_session(self, code):
"""Create point session and entries."""
from pypoint import PointSession
flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN]
client_id = flow[CLIENT_ID]
client_secret = flow[CLIENT_SECRET]
point_session = PointSession(
client_id,
client_secret=client_secret,
)
token = await self.hass.async_add_executor_job(
point_session.get_access_token, code)
_LOGGER.debug("Got new token")
if not point_session.is_authorized:
_LOGGER.error('Authentication Error')
return self.async_abort(reason='auth_error')
_LOGGER.info('Successfully authenticated Point')
user_email = point_session.user().get('email') or ""
return self.async_create_entry(
title=user_email,
data={
'token': token,
'refresh_args': {
'client_id': client_id,
'client_secret': client_secret
}
},
)
class MinutAuthCallbackView(HomeAssistantView):
"""Minut Authorization Callback View."""
requires_auth = False
url = AUTH_CALLBACK_PATH
name = AUTH_CALLBACK_NAME
@staticmethod
async def get(request):
"""Receive authorization code."""
hass = request.app['hass']
if 'code' in request.query:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': 'code'},
data=request.query['code'],
))
return "OK!"

View file

@ -0,0 +1,15 @@
"""Define constants for the Point component."""
from datetime import timedelta
DOMAIN = 'point'
CLIENT_ID = 'client_id'
CLIENT_SECRET = 'client_secret'
SCAN_INTERVAL = timedelta(minutes=1)
CONF_WEBHOOK_URL = 'webhook_url'
EVENT_RECEIVED = 'point_webhook_received'
SIGNAL_UPDATE_ENTITY = 'point_update'
SIGNAL_WEBHOOK = 'point_webhook'
NEW_DEVICE = 'new_device'

View file

@ -0,0 +1,32 @@
{
"config": {
"title": "Minut Point",
"step": {
"user": {
"title": "Authentication Provider",
"description": "Pick via which authentication provider you want to authenticate with Point.",
"data": {
"flow_impl": "Provider"
}
},
"auth": {
"title": "Authenticate Point",
"description": "Please follow the link below and <b>Accept</b> access to your Minut account, then come back and press <b>Submit</b> below.\n\n[Link]({authorization_url})"
}
},
"create_entry": {
"default": "Successfully authenticated with Minut for your Point device(s)"
},
"error": {
"no_token": "Not authenticated with Minut",
"follow_link": "Please follow the link and authenticate before pressing Submit"
},
"abort": {
"already_setup": "You can only configure a Point account.",
"external_setup": "Point successfully configured from another flow.",
"no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/).",
"authorize_url_timeout": "Timeout generating authorize url.",
"authorize_url_fail": "Unknown error generating an authorize url."
}
}
}

View file

@ -0,0 +1,68 @@
"""
Support for Minut Point.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.point/
"""
import logging
from homeassistant.components.point import MinutPointEntity
from homeassistant.components.point.const import (
DOMAIN as POINT_DOMAIN, NEW_DEVICE)
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS)
from homeassistant.core import callback
from homeassistant.util.dt import parse_datetime
_LOGGER = logging.getLogger(__name__)
DEVICE_CLASS_SOUND = 'sound_level'
SENSOR_TYPES = {
DEVICE_CLASS_TEMPERATURE: (None, 1, TEMP_CELSIUS),
DEVICE_CLASS_PRESSURE: (None, 0, 'hPa'),
DEVICE_CLASS_HUMIDITY: (None, 1, '%'),
DEVICE_CLASS_SOUND: ('mdi:ear-hearing', 1, 'dBa'),
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Point's sensors based on a config entry."""
device_id = config_entry.data[NEW_DEVICE]
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
async_add_entities((MinutPointSensor(client, device_id, sensor_type)
for sensor_type in SENSOR_TYPES), True)
class MinutPointSensor(MinutPointEntity):
"""The platform class required by Home Assistant."""
def __init__(self, point_client, device_id, device_class):
"""Initialize the entity."""
super().__init__(point_client, device_id, device_class)
self._device_prop = SENSOR_TYPES[device_class]
@callback
def _update_callback(self):
"""Update the value of the sensor."""
if self.is_updated:
_LOGGER.debug('Update sensor value for %s', self)
self._value = self.device.sensor(self.device_class)
self._updated = parse_datetime(self.device.last_update)
self.async_schedule_update_ha_state()
@property
def icon(self):
"""Return the icon representation."""
return self._device_prop[0]
@property
def state(self):
"""Return the state of the sensor."""
return round(self.value, self._device_prop[1])
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._device_prop[2]

View file

@ -149,6 +149,7 @@ FLOWS = [
'mqtt',
'nest',
'openuv',
'point',
'rainmachine',
'simplisafe',
'smhi',

View file

@ -1095,6 +1095,9 @@ pyowm==2.9.0
# homeassistant.components.media_player.pjlink
pypjlink2==1.2.0
# homeassistant.components.point
pypoint==1.0.5
# homeassistant.components.sensor.pollen
pypollencom==2.2.2

View file

@ -0,0 +1 @@
"""Tests for the Point component."""

View file

@ -0,0 +1,147 @@
"""Tests for the Point config flow."""
import asyncio
from unittest.mock import Mock, patch
import pytest
from homeassistant import data_entry_flow
from homeassistant.components.point import DOMAIN, config_flow
from tests.common import MockDependency, mock_coro
def init_config_flow(hass, side_effect=None):
"""Init a configuration flow."""
config_flow.register_flow_implementation(hass, DOMAIN, 'id', 'secret')
flow = config_flow.PointFlowHandler()
flow._get_authorization_url = Mock( # pylint: disable=W0212
return_value=mock_coro('https://example.com'),
side_effect=side_effect)
flow.hass = hass
return flow
@pytest.fixture
def is_authorized():
"""Set PointSession authorized."""
return True
@pytest.fixture
def mock_pypoint(is_authorized): # pylint: disable=W0621
"""Mock pypoint."""
with MockDependency('pypoint') as mock_pypoint_:
mock_pypoint_.PointSession().get_access_token.return_value = {
'access_token': 'boo'
}
mock_pypoint_.PointSession().is_authorized = is_authorized
mock_pypoint_.PointSession().user.return_value = {
'email': 'john.doe@example.com'
}
yield mock_pypoint_
async def test_abort_if_no_implementation_registered(hass):
"""Test we abort if no implementation is registered."""
flow = config_flow.PointFlowHandler()
flow.hass = hass
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'no_flows'
async def test_abort_if_already_setup(hass):
"""Test we abort if Point is already setup."""
flow = init_config_flow(hass)
with patch.object(hass.config_entries, 'async_entries', return_value=[{}]):
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'already_setup'
with patch.object(hass.config_entries, 'async_entries', return_value=[{}]):
result = await flow.async_step_import()
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'already_setup'
async def test_full_flow_implementation(hass, mock_pypoint): # noqa pylint: disable=W0621
"""Test registering an implementation and finishing flow works."""
config_flow.register_flow_implementation(hass, 'test-other', None, None)
flow = init_config_flow(hass)
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'user'
result = await flow.async_step_user({'flow_impl': 'test'})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'auth'
assert result['description_placeholders'] == {
'authorization_url': 'https://example.com',
}
result = await flow.async_step_code('123ABC')
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data']['refresh_args'] == {
'client_id': 'id',
'client_secret': 'secret'
}
assert result['title'] == 'john.doe@example.com'
assert result['data']['token'] == {'access_token': 'boo'}
async def test_step_import(hass, mock_pypoint): # pylint: disable=W0621
"""Test that we trigger import when configuring with client."""
flow = init_config_flow(hass)
result = await flow.async_step_import()
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'auth'
@pytest.mark.parametrize('is_authorized', [False])
async def test_wrong_code_flow_implementation(hass, mock_pypoint): # noqa pylint: disable=W0621
"""Test wrong code."""
flow = init_config_flow(hass)
result = await flow.async_step_code('123ABC')
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'auth_error'
async def test_not_pick_implementation_if_only_one(hass):
"""Test we allow picking implementation if we have one flow_imp."""
flow = init_config_flow(hass)
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'auth'
async def test_abort_if_timeout_generating_auth_url(hass):
"""Test we abort if generating authorize url fails."""
flow = init_config_flow(hass, side_effect=asyncio.TimeoutError)
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'authorize_url_timeout'
async def test_abort_if_exception_generating_auth_url(hass):
"""Test we abort if generating authorize url blows up."""
flow = init_config_flow(hass, side_effect=ValueError)
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'authorize_url_fail'
async def test_abort_no_code(hass):
"""Test if no code is given to step_code."""
flow = init_config_flow(hass)
result = await flow.async_step_code()
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'no_code'