Automatically generate config flow list (#23802)
* Add config flow to manifest.json * Still load config flows via config flow platform * Fix typo * Lint * Update config_flows.py" * Catch import error when setting up entry * Lint * Fix tests * Fix imports * Lint * Fix Unifi tests * Fix translation test * Add homekit_controller config flowpull/23845/head
parent
b8cbd39985
commit
1e22c8daca
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "ambiclimate",
|
||||
"name": "Ambiclimate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambiclimate",
|
||||
"requirements": [
|
||||
"ambiclimate==0.1.1"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "ambient_station",
|
||||
"name": "Ambient station",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambient_station",
|
||||
"requirements": [
|
||||
"aioambient==0.3.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "axis",
|
||||
"name": "Axis",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/axis",
|
||||
"requirements": ["axis==22"],
|
||||
"dependencies": [],
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
"""Component to embed Google Cast."""
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
DOMAIN = 'cast'
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
|
@ -23,15 +22,3 @@ async def async_setup_entry(hass, entry):
|
|||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||
entry, 'media_player'))
|
||||
return True
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pychromecast.discovery import discover_chromecasts
|
||||
|
||||
return await hass.async_add_executor_job(discover_chromecasts)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Google Cast', _async_has_devices,
|
||||
config_entries.CONN_CLASS_LOCAL_PUSH)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
"""Config flow for Cast."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pychromecast.discovery import discover_chromecasts
|
||||
|
||||
return await hass.async_add_executor_job(discover_chromecasts)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Google Cast', _async_has_devices,
|
||||
config_entries.CONN_CLASS_LOCAL_PUSH)
|
|
@ -0,0 +1,3 @@
|
|||
"""Consts for Cast integration."""
|
||||
|
||||
DOMAIN = 'cast'
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "cast",
|
||||
"name": "Cast",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/cast",
|
||||
"requirements": [
|
||||
"pychromecast==3.2.1"
|
||||
|
|
|
@ -6,6 +6,7 @@ from homeassistant.components.http import HomeAssistantView
|
|||
from homeassistant.exceptions import Unauthorized
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from homeassistant.generated import config_flows
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
|
@ -172,7 +173,7 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
|
|||
|
||||
async def get(self, request):
|
||||
"""List available flow handlers."""
|
||||
return self.json(config_entries.FLOWS)
|
||||
return self.json(config_flows.FLOWS)
|
||||
|
||||
|
||||
class OptionManagerFlowIndexView(FlowManagerIndexView):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "daikin",
|
||||
"name": "Daikin",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/daikin",
|
||||
"requirements": [
|
||||
"pydaikin==1.4.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "deconz",
|
||||
"name": "Deconz",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/deconz",
|
||||
"requirements": [
|
||||
"pydeconz==58"
|
||||
|
|
|
@ -8,9 +8,10 @@ from homeassistant.const import CONF_WEBHOOK_ID
|
|||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import intent, template, config_entry_flow
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN
|
||||
|
||||
DOMAIN = 'dialogflow'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SOURCE = "Home Assistant Dialogflow"
|
||||
|
||||
|
@ -83,16 +84,6 @@ async def async_unload_entry(hass, entry):
|
|||
async_remove_entry = config_entry_flow.webhook_async_remove_entry
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Dialogflow Webhook',
|
||||
{
|
||||
'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook',
|
||||
'docs_url': 'https://www.home-assistant.io/components/dialogflow/'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def dialogflow_error_response(message, error):
|
||||
"""Return a response saying the error message."""
|
||||
dialogflow_response = DialogflowResponse(message['result']['parameters'])
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
"""Config flow for DialogFlow."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Dialogflow Webhook',
|
||||
{
|
||||
'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook',
|
||||
'docs_url': 'https://www.home-assistant.io/components/dialogflow/'
|
||||
}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for DialogFlow."""
|
||||
|
||||
DOMAIN = "dialogflow"
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "dialogflow",
|
||||
"name": "Dialogflow",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/dialogflow",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "emulated_roku",
|
||||
"name": "Emulated roku",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/emulated_roku",
|
||||
"requirements": [
|
||||
"emulated_roku==0.1.8"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "esphome",
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/esphome",
|
||||
"requirements": [
|
||||
"aioesphomeapi==2.0.1"
|
||||
|
|
|
@ -12,10 +12,11 @@ from homeassistant.helpers import config_entry_flow
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.util import slugify
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'geofency'
|
||||
CONF_MOBILE_BEACONS = 'mobile_beacons'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
|
@ -134,12 +135,3 @@ async def async_unload_entry(hass, entry):
|
|||
|
||||
# pylint: disable=invalid-name
|
||||
async_remove_entry = config_entry_flow.webhook_async_remove_entry
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Geofency Webhook',
|
||||
{
|
||||
'docs_url': 'https://www.home-assistant.io/components/geofency/'
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
"""Config flow for Geofency."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Geofency Webhook',
|
||||
{
|
||||
'docs_url': 'https://www.home-assistant.io/components/geofency/'
|
||||
}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for Geofency."""
|
||||
|
||||
DOMAIN = 'geofency'
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "geofency",
|
||||
"name": "Geofency",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/geofency",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
|
|
|
@ -11,10 +11,10 @@ from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, \
|
|||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'gpslogger'
|
||||
TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN)
|
||||
|
||||
ATTR_ALTITUDE = 'altitude'
|
||||
|
@ -105,12 +105,3 @@ async def async_unload_entry(hass, entry):
|
|||
|
||||
# pylint: disable=invalid-name
|
||||
async_remove_entry = config_entry_flow.webhook_async_remove_entry
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'GPSLogger Webhook',
|
||||
{
|
||||
'docs_url': 'https://www.home-assistant.io/components/gpslogger/'
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
"""Config flow for GPSLogger."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'GPSLogger Webhook',
|
||||
{
|
||||
'docs_url': 'https://www.home-assistant.io/components/gpslogger/'
|
||||
}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for GPSLogger."""
|
||||
|
||||
DOMAIN = 'gpslogger'
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "gpslogger",
|
||||
"name": "Gpslogger",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/gpslogger",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "hangouts",
|
||||
"name": "Hangouts",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/hangouts",
|
||||
"requirements": [
|
||||
"hangups==0.4.9"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "heos",
|
||||
"name": "HEOS",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/heos",
|
||||
"requirements": [
|
||||
"pyheos==0.5.2"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "homekit_controller",
|
||||
"name": "Homekit controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/homekit_controller",
|
||||
"requirements": [
|
||||
"homekit[IP]==0.14.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "homematicip_cloud",
|
||||
"name": "Homematicip cloud",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/homematicip_cloud",
|
||||
"requirements": [
|
||||
"homematicip==0.10.7"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "hue",
|
||||
"name": "Philips Hue",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/hue",
|
||||
"requirements": [
|
||||
"aiohue==1.9.1"
|
||||
|
|
|
@ -8,6 +8,7 @@ import voluptuous as vol
|
|||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -21,8 +22,6 @@ ATTR_VALUE3 = 'value3'
|
|||
|
||||
CONF_KEY = 'key'
|
||||
|
||||
DOMAIN = 'ifttt'
|
||||
|
||||
SERVICE_TRIGGER = 'trigger'
|
||||
|
||||
SERVICE_TRIGGER_SCHEMA = vol.Schema({
|
||||
|
@ -108,13 +107,3 @@ async def async_unload_entry(hass, entry):
|
|||
|
||||
# pylint: disable=invalid-name
|
||||
async_remove_entry = config_entry_flow.webhook_async_remove_entry
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'IFTTT Webhook',
|
||||
{
|
||||
'applet_url': 'https://ifttt.com/maker_webhooks',
|
||||
'docs_url': 'https://www.home-assistant.io/components/ifttt/'
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
"""Config flow for IFTTT."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'IFTTT Webhook',
|
||||
{
|
||||
'applet_url': 'https://ifttt.com/maker_webhooks',
|
||||
'docs_url': 'https://www.home-assistant.io/components/ifttt/'
|
||||
}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for IFTTT."""
|
||||
|
||||
DOMAIN = "ifttt"
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "ifttt",
|
||||
"name": "Ifttt",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ifttt",
|
||||
"requirements": [
|
||||
"pyfttt==0.3"
|
||||
|
|
|
@ -9,8 +9,7 @@ from homeassistant.components.http import HomeAssistantView
|
|||
from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_entry_flow, config_validation as cv, discovery)
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -279,8 +278,3 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
|||
HTTP_INTERNAL_SERVER_ERROR)
|
||||
|
||||
return self.json({"status": "registered"})
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Home Assistant iOS', lambda *_: True,
|
||||
config_entries.CONN_CLASS_CLOUD_PUSH)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
"""Config flow for iOS."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Home Assistant iOS', lambda *_: True,
|
||||
config_entries.CONN_CLASS_CLOUD_PUSH)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for iOS."""
|
||||
|
||||
DOMAIN = "ios"
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "ios",
|
||||
"name": "Ios",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ios",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "ipma",
|
||||
"name": "Ipma",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ipma",
|
||||
"requirements": [
|
||||
"pyipma==1.2.1"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "iqvia",
|
||||
"name": "IQVIA",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/iqvia",
|
||||
"requirements": [
|
||||
"numpy==1.16.3",
|
||||
|
|
|
@ -4,10 +4,10 @@ import homeassistant.helpers.config_validation as cv
|
|||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
DOMAIN = 'lifx'
|
||||
CONF_SERVER = 'server'
|
||||
CONF_BROADCAST = 'broadcast'
|
||||
|
||||
|
@ -55,15 +55,3 @@ async def async_unload_entry(hass, entry):
|
|||
await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
import aiolifx
|
||||
|
||||
lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan()
|
||||
return len(lifx_ip_addresses) > 0
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'LIFX', _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
"""Config flow flow LIFX."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
import aiolifx
|
||||
|
||||
lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan()
|
||||
return len(lifx_ip_addresses) > 0
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'LIFX', _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for LIFX."""
|
||||
|
||||
DOMAIN = 'lifx'
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "lifx",
|
||||
"name": "Lifx",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/lifx",
|
||||
"requirements": [
|
||||
"aiolifx==0.6.7",
|
||||
|
|
|
@ -145,12 +145,3 @@ async def async_unload_entry(hass, entry):
|
|||
|
||||
# pylint: disable=invalid-name
|
||||
async_remove_entry = config_entry_flow.webhook_async_remove_entry
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Locative Webhook',
|
||||
{
|
||||
'docs_url': 'https://www.home-assistant.io/components/locative/'
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
"""Config flow for Locative."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Locative Webhook',
|
||||
{
|
||||
'docs_url': 'https://www.home-assistant.io/components/locative/'
|
||||
}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for Locative."""
|
||||
|
||||
DOMAIN = "locative"
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "locative",
|
||||
"name": "Locative",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/locative",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "logi_circle",
|
||||
"name": "Logi Circle",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/logi_circle",
|
||||
"requirements": ["logi_circle==0.2.2"],
|
||||
"dependencies": ["ffmpeg"],
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "luftdaten",
|
||||
"name": "Luftdaten",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/luftdaten",
|
||||
"requirements": [
|
||||
"luftdaten==0.3.4"
|
||||
|
|
|
@ -10,12 +10,14 @@ import homeassistant.helpers.config_validation as cv
|
|||
from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_WEBHOOK_ID
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SANDBOX = 'sandbox'
|
||||
|
||||
DEFAULT_SANDBOX = False
|
||||
DOMAIN = 'mailgun'
|
||||
|
||||
MESSAGE_RECEIVED = '{}_message_received'.format(DOMAIN)
|
||||
|
||||
|
@ -90,13 +92,3 @@ async def async_unload_entry(hass, entry):
|
|||
|
||||
# pylint: disable=invalid-name
|
||||
async_remove_entry = config_entry_flow.webhook_async_remove_entry
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Mailgun Webhook',
|
||||
{
|
||||
'mailgun_url': 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', # noqa: E501 pylint: disable=line-too-long
|
||||
'docs_url': 'https://www.home-assistant.io/components/mailgun/'
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
"""Config flow for Mailgun."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Mailgun Webhook',
|
||||
{
|
||||
'mailgun_url': 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', # noqa: E501 pylint: disable=line-too-long
|
||||
'docs_url': 'https://www.home-assistant.io/components/mailgun/'
|
||||
}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for Mailgun."""
|
||||
|
||||
DOMAIN = "mailgun"
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "mailgun",
|
||||
"name": "Mailgun",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/mailgun",
|
||||
"requirements": [
|
||||
"pymailgunner==1.4"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
"""Integrates Native Apps to Home Assistant."""
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.components.webhook import async_register as webhook_register
|
||||
from homeassistant.helpers import device_registry as dr, discovery
|
||||
|
@ -91,26 +90,3 @@ async def async_setup_entry(hass, entry):
|
|||
hass.config_entries.async_forward_entry_setup(entry, DATA_SENSOR))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class MobileAppFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a Mobile App config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
placeholders = {
|
||||
'apps_url':
|
||||
'https://www.home-assistant.io/components/mobile_app/#apps'
|
||||
}
|
||||
|
||||
return self.async_abort(reason='install_app',
|
||||
description_placeholders=placeholders)
|
||||
|
||||
async def async_step_registration(self, user_input=None):
|
||||
"""Handle a flow initialized during registration."""
|
||||
return self.async_create_entry(title=user_input[ATTR_DEVICE_NAME],
|
||||
data=user_input)
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
"""Config flow for Mobile App."""
|
||||
from homeassistant import config_entries
|
||||
from .const import DOMAIN, ATTR_DEVICE_NAME
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class MobileAppFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a Mobile App config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
placeholders = {
|
||||
'apps_url':
|
||||
'https://www.home-assistant.io/components/mobile_app/#apps'
|
||||
}
|
||||
|
||||
return self.async_abort(reason='install_app',
|
||||
description_placeholders=placeholders)
|
||||
|
||||
async def async_step_registration(self, user_input=None):
|
||||
"""Handle a flow initialized during registration."""
|
||||
return self.async_create_entry(title=user_input[ATTR_DEVICE_NAME],
|
||||
data=user_input)
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "mobile_app",
|
||||
"name": "Home Assistant Mobile App Support",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/mobile_app",
|
||||
"requirements": [
|
||||
"PyNaCl==1.3.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "mqtt",
|
||||
"name": "MQTT",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/mqtt",
|
||||
"requirements": [
|
||||
"hbmqtt==0.9.4",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "nest",
|
||||
"name": "Nest",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/nest",
|
||||
"requirements": [
|
||||
"python-nest==4.1.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "openuv",
|
||||
"name": "Openuv",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/openuv",
|
||||
"requirements": [
|
||||
"pyopenuv==1.0.9"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "owntracks",
|
||||
"name": "Owntracks",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/owntracks",
|
||||
"requirements": [
|
||||
"PyNaCl==1.3.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "point",
|
||||
"name": "Point",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/point",
|
||||
"requirements": [
|
||||
"pypoint==1.1.1"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "ps4",
|
||||
"name": "Ps4",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ps4",
|
||||
"requirements": [
|
||||
"pyps4-homeassistant==0.7.3"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "rainmachine",
|
||||
"name": "Rainmachine",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/rainmachine",
|
||||
"requirements": [
|
||||
"regenmaschine==1.4.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "simplisafe",
|
||||
"name": "Simplisafe",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/simplisafe",
|
||||
"requirements": [
|
||||
"simplisafe-python==3.4.1"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "smartthings",
|
||||
"name": "Smartthings",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/smartthings",
|
||||
"requirements": [
|
||||
"pysmartapp==0.3.2",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "smhi",
|
||||
"name": "Smhi",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/smhi",
|
||||
"requirements": [
|
||||
"smhi-pkg==1.0.10"
|
||||
|
|
|
@ -5,10 +5,11 @@ import voluptuous as vol
|
|||
from homeassistant import config_entries
|
||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.const import CONF_HOSTS, ATTR_ENTITY_ID, ATTR_TIME
|
||||
from homeassistant.helpers import config_entry_flow, config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
DOMAIN = 'sonos'
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
CONF_ADVERTISE_ADDR = 'advertise_addr'
|
||||
CONF_INTERFACE_ADDR = 'interface_addr'
|
||||
|
@ -141,14 +142,3 @@ async def async_setup_entry(hass, entry):
|
|||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||
entry, MP_DOMAIN))
|
||||
return True
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
import pysonos
|
||||
|
||||
return await hass.async_add_executor_job(pysonos.discover)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Sonos', _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
"""Config flow for SONOS."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
import pysonos
|
||||
|
||||
return await hass.async_add_executor_job(pysonos.discover)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Sonos', _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for Sonos."""
|
||||
|
||||
DOMAIN = "sonos"
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "sonos",
|
||||
"name": "Sonos",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/sonos",
|
||||
"requirements": [
|
||||
"pysonos==0.0.12"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "tellduslive",
|
||||
"name": "Tellduslive",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/tellduslive",
|
||||
"requirements": [
|
||||
"tellduslive==0.10.10"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "toon",
|
||||
"name": "Toon",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/toon",
|
||||
"requirements": [
|
||||
"toonapilib==3.2.2"
|
||||
|
|
|
@ -5,14 +5,12 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .config_flow import async_get_devices
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'tplink'
|
||||
|
||||
TPLINK_HOST_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string
|
||||
})
|
||||
|
@ -34,16 +32,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pyHS100 import Discover
|
||||
|
||||
def discover():
|
||||
devs = Discover.discover()
|
||||
return devs
|
||||
return await hass.async_add_executor_job(discover)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the TP-Link component."""
|
||||
conf = config.get(DOMAIN)
|
||||
|
@ -74,7 +62,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
# If initialized from configure integrations, there's no config
|
||||
# so we default here to True
|
||||
if config_data is None or config_data[CONF_DISCOVERY]:
|
||||
devs = await _async_has_devices(hass)
|
||||
devs = await async_get_devices(hass)
|
||||
_LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devs))
|
||||
devices.update(devs)
|
||||
|
||||
|
@ -149,9 +137,3 @@ async def async_unload_entry(hass, entry):
|
|||
# We were not able to unload the platforms, either because there
|
||||
# were none or one of the forward_unloads failed.
|
||||
return False
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(DOMAIN,
|
||||
'TP-Link Smart Home',
|
||||
_async_has_devices,
|
||||
config_entries.CONN_CLASS_LOCAL_POLL)
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
"""Config flow for TP-Link."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_get_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pyHS100 import Discover
|
||||
|
||||
def discover():
|
||||
devs = Discover.discover()
|
||||
return devs
|
||||
return await hass.async_add_executor_job(discover)
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(DOMAIN,
|
||||
'TP-Link Smart Home',
|
||||
async_get_devices,
|
||||
config_entries.CONN_CLASS_LOCAL_POLL)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for TP-Link."""
|
||||
|
||||
DOMAIN = "tplink"
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "tplink",
|
||||
"name": "Tplink",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/tplink",
|
||||
"requirements": [
|
||||
"pyHS100==0.3.5",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "tradfri",
|
||||
"name": "Tradfri",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/tradfri",
|
||||
"requirements": [
|
||||
"pytradfri[async]==6.0.1"
|
||||
|
|
|
@ -4,8 +4,7 @@ import voluptuous as vol
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
DOMAIN = 'twilio'
|
||||
from .const import DOMAIN
|
||||
|
||||
CONF_ACCOUNT_SID = 'account_sid'
|
||||
CONF_AUTH_TOKEN = 'auth_token'
|
||||
|
@ -60,14 +59,3 @@ async def async_unload_entry(hass, entry):
|
|||
|
||||
# pylint: disable=invalid-name
|
||||
async_remove_entry = config_entry_flow.webhook_async_remove_entry
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Twilio Webhook',
|
||||
{
|
||||
'twilio_url':
|
||||
'https://www.twilio.com/docs/glossary/what-is-a-webhook',
|
||||
'docs_url': 'https://www.home-assistant.io/components/twilio/'
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
"""Config flow for Twilio."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
'Twilio Webhook',
|
||||
{
|
||||
'twilio_url':
|
||||
'https://www.twilio.com/docs/glossary/what-is-a-webhook',
|
||||
'docs_url': 'https://www.home-assistant.io/components/twilio/'
|
||||
}
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
"""Const for Twilio."""
|
||||
|
||||
DOMAIN = "twilio"
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "twilio",
|
||||
"name": "Twilio",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/twilio",
|
||||
"requirements": [
|
||||
"twilio==6.19.1"
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
"""Support for devices connected to UniFi POE."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
|
||||
from .const import (CONF_CONTROLLER, CONF_POE_CONTROL, CONF_SITE_ID,
|
||||
CONTROLLER_ID, DOMAIN, LOGGER)
|
||||
from .controller import UniFiController, get_controller
|
||||
from .errors import (
|
||||
AlreadyConfigured, AuthenticationRequired, CannotConnect, UserLevel)
|
||||
|
||||
DEFAULT_PORT = 8443
|
||||
DEFAULT_SITE_ID = 'default'
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
from .const import CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID, DOMAIN
|
||||
from .controller import UniFiController
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
|
@ -64,116 +53,3 @@ async def async_unload_entry(hass, config_entry):
|
|||
)
|
||||
controller = hass.data[DOMAIN].pop(controller_id)
|
||||
return await controller.async_reset()
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class UnifiFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a UniFi config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the UniFi flow."""
|
||||
self.config = None
|
||||
self.desc = None
|
||||
self.sites = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
try:
|
||||
self.config = {
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_PORT: user_input.get(CONF_PORT),
|
||||
CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL),
|
||||
CONF_SITE_ID: DEFAULT_SITE_ID,
|
||||
}
|
||||
controller = await get_controller(self.hass, **self.config)
|
||||
|
||||
self.sites = await controller.sites()
|
||||
|
||||
return await self.async_step_site()
|
||||
|
||||
except AuthenticationRequired:
|
||||
errors['base'] = 'faulty_credentials'
|
||||
|
||||
except CannotConnect:
|
||||
errors['base'] = 'service_unavailable'
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
LOGGER.error(
|
||||
'Unknown error connecting with UniFi Controller at %s',
|
||||
user_input[CONF_HOST])
|
||||
return self.async_abort(reason='unknown')
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Optional(
|
||||
CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_site(self, user_input=None):
|
||||
"""Select site to control."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
try:
|
||||
desc = user_input.get(CONF_SITE_ID, self.desc)
|
||||
for site in self.sites.values():
|
||||
if desc == site['desc']:
|
||||
if site['role'] != 'admin':
|
||||
raise UserLevel
|
||||
self.config[CONF_SITE_ID] = site['name']
|
||||
break
|
||||
|
||||
for entry in self._async_current_entries():
|
||||
controller = entry.data[CONF_CONTROLLER]
|
||||
if controller[CONF_HOST] == self.config[CONF_HOST] and \
|
||||
controller[CONF_SITE_ID] == self.config[CONF_SITE_ID]:
|
||||
raise AlreadyConfigured
|
||||
|
||||
data = {
|
||||
CONF_CONTROLLER: self.config,
|
||||
CONF_POE_CONTROL: True
|
||||
}
|
||||
|
||||
return self.async_create_entry(
|
||||
title=desc,
|
||||
data=data
|
||||
)
|
||||
|
||||
except AlreadyConfigured:
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
except UserLevel:
|
||||
return self.async_abort(reason='user_privilege')
|
||||
|
||||
if len(self.sites) == 1:
|
||||
self.desc = next(iter(self.sites.values()))['desc']
|
||||
return await self.async_step_site(user_input={})
|
||||
|
||||
sites = []
|
||||
for site in self.sites.values():
|
||||
sites.append(site['desc'])
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='site',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_SITE_ID): vol.In(sites)
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
"""Config flow for Unifi."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL)
|
||||
|
||||
from .const import (CONF_CONTROLLER, CONF_POE_CONTROL, CONF_SITE_ID,
|
||||
DOMAIN, LOGGER)
|
||||
from .controller import get_controller
|
||||
from .errors import (
|
||||
AlreadyConfigured, AuthenticationRequired, CannotConnect, UserLevel)
|
||||
|
||||
|
||||
DEFAULT_PORT = 8443
|
||||
DEFAULT_SITE_ID = 'default'
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class UnifiFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a UniFi config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the UniFi flow."""
|
||||
self.config = None
|
||||
self.desc = None
|
||||
self.sites = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
try:
|
||||
self.config = {
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_PORT: user_input.get(CONF_PORT),
|
||||
CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL),
|
||||
CONF_SITE_ID: DEFAULT_SITE_ID,
|
||||
}
|
||||
controller = await get_controller(self.hass, **self.config)
|
||||
|
||||
self.sites = await controller.sites()
|
||||
|
||||
return await self.async_step_site()
|
||||
|
||||
except AuthenticationRequired:
|
||||
errors['base'] = 'faulty_credentials'
|
||||
|
||||
except CannotConnect:
|
||||
errors['base'] = 'service_unavailable'
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
LOGGER.error(
|
||||
'Unknown error connecting with UniFi Controller at %s',
|
||||
user_input[CONF_HOST])
|
||||
return self.async_abort(reason='unknown')
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Optional(
|
||||
CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_site(self, user_input=None):
|
||||
"""Select site to control."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
try:
|
||||
desc = user_input.get(CONF_SITE_ID, self.desc)
|
||||
for site in self.sites.values():
|
||||
if desc == site['desc']:
|
||||
if site['role'] != 'admin':
|
||||
raise UserLevel
|
||||
self.config[CONF_SITE_ID] = site['name']
|
||||
break
|
||||
|
||||
for entry in self._async_current_entries():
|
||||
controller = entry.data[CONF_CONTROLLER]
|
||||
if controller[CONF_HOST] == self.config[CONF_HOST] and \
|
||||
controller[CONF_SITE_ID] == self.config[CONF_SITE_ID]:
|
||||
raise AlreadyConfigured
|
||||
|
||||
data = {
|
||||
CONF_CONTROLLER: self.config,
|
||||
CONF_POE_CONTROL: True
|
||||
}
|
||||
|
||||
return self.async_create_entry(
|
||||
title=desc,
|
||||
data=data
|
||||
)
|
||||
|
||||
except AlreadyConfigured:
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
except UserLevel:
|
||||
return self.async_abort(reason='user_privilege')
|
||||
|
||||
if len(self.sites) == 1:
|
||||
self.desc = next(iter(self.sites.values()))['desc']
|
||||
return await self.async_step_site(user_input={})
|
||||
|
||||
sites = []
|
||||
for site in self.sites.values():
|
||||
sites.append(site['desc'])
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='site',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_SITE_ID): vol.In(sites)
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "unifi",
|
||||
"name": "Unifi",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/unifi",
|
||||
"requirements": [
|
||||
"aiounifi==4",
|
||||
|
|
|
@ -7,7 +7,6 @@ import voluptuous as vol
|
|||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import dispatcher
|
||||
|
@ -204,10 +203,3 @@ async def async_unload_entry(hass: HomeAssistantType,
|
|||
dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN,
|
||||
'UPnP/IGD',
|
||||
Device.async_discover,
|
||||
config_entries.CONN_CLASS_LOCAL_POLL)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
"""Config flow for UPNP."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
|
||||
from .const import DOMAIN
|
||||
from .device import Device
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN,
|
||||
'UPnP/IGD',
|
||||
Device.async_discover,
|
||||
config_entries.CONN_CLASS_LOCAL_POLL)
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "upnp",
|
||||
"name": "Upnp",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/upnp",
|
||||
"requirements": [
|
||||
"async-upnp-client==0.14.7"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "zha",
|
||||
"name": "Zigbee Home Automation",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/zha",
|
||||
"requirements": [
|
||||
"bellows-homeassistant==0.7.3",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "zone",
|
||||
"name": "Zone",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/zone",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"domain": "zwave",
|
||||
"name": "Z-Wave",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/zwave",
|
||||
"requirements": [
|
||||
"homeassistant-pyozw==0.1.4",
|
||||
|
|
|
@ -140,57 +140,6 @@ SOURCE_DISCOVERY = 'discovery'
|
|||
SOURCE_IMPORT = 'import'
|
||||
|
||||
HANDLERS = Registry()
|
||||
# Components that have config flows. In future we will auto-generate this list.
|
||||
FLOWS = [
|
||||
'ambiclimate',
|
||||
'ambient_station',
|
||||
'axis',
|
||||
'cast',
|
||||
'daikin',
|
||||
'deconz',
|
||||
'dialogflow',
|
||||
'esphome',
|
||||
'emulated_roku',
|
||||
'geofency',
|
||||
'gpslogger',
|
||||
'hangouts',
|
||||
'heos',
|
||||
'homekit_controller',
|
||||
'homematicip_cloud',
|
||||
'hue',
|
||||
'ifttt',
|
||||
'ios',
|
||||
'ipma',
|
||||
'iqvia',
|
||||
'lifx',
|
||||
'locative',
|
||||
'logi_circle',
|
||||
'luftdaten',
|
||||
'mailgun',
|
||||
'mobile_app',
|
||||
'mqtt',
|
||||
'nest',
|
||||
'openuv',
|
||||
'owntracks',
|
||||
'point',
|
||||
'ps4',
|
||||
'rainmachine',
|
||||
'simplisafe',
|
||||
'smartthings',
|
||||
'smhi',
|
||||
'sonos',
|
||||
'tellduslive',
|
||||
'toon',
|
||||
'tplink',
|
||||
'tradfri',
|
||||
'twilio',
|
||||
'unifi',
|
||||
'upnp',
|
||||
'zha',
|
||||
'zone',
|
||||
'zwave',
|
||||
]
|
||||
|
||||
|
||||
STORAGE_KEY = 'core.config_entries'
|
||||
STORAGE_VERSION = 1
|
||||
|
@ -299,7 +248,17 @@ class ConfigEntry:
|
|||
if integration is None:
|
||||
integration = await loader.async_get_integration(hass, self.domain)
|
||||
|
||||
component = integration.get_component()
|
||||
try:
|
||||
component = integration.get_component()
|
||||
if self.domain == integration.domain:
|
||||
integration.get_platform('config_flow')
|
||||
except ImportError as err:
|
||||
_LOGGER.error(
|
||||
'Error importing integration %s to set up %s config entry: %s',
|
||||
integration.domain, self.domain, err)
|
||||
if self.domain == integration.domain:
|
||||
self.state = ENTRY_STATE_SETUP_ERROR
|
||||
return
|
||||
|
||||
# Perform migration
|
||||
if integration.domain == self.domain:
|
||||
|
@ -422,7 +381,8 @@ class ConfigEntry:
|
|||
if self.version == handler.VERSION:
|
||||
return True
|
||||
|
||||
component = getattr(hass.components, self.domain)
|
||||
integration = await loader.async_get_integration(hass, self.domain)
|
||||
component = integration.get_component()
|
||||
supports_migrate = hasattr(component, 'async_migrate_entry')
|
||||
if not supports_migrate:
|
||||
_LOGGER.error("Migration handler not found for entry %s for %s",
|
||||
|
@ -430,7 +390,9 @@ class ConfigEntry:
|
|||
return False
|
||||
|
||||
try:
|
||||
result = await component.async_migrate_entry(hass, self)
|
||||
result = await component.async_migrate_entry( # type: ignore
|
||||
hass, self
|
||||
)
|
||||
if not isinstance(result, bool):
|
||||
_LOGGER.error('%s.async_migrate_entry did not return boolean',
|
||||
self.domain)
|
||||
|
@ -441,7 +403,7 @@ class ConfigEntry:
|
|||
return result
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error migrating entry %s for %s',
|
||||
self.title, component.DOMAIN)
|
||||
self.title, self.domain)
|
||||
return False
|
||||
|
||||
def add_update_listener(self, listener: Callable) -> Callable:
|
||||
|
@ -714,10 +676,10 @@ class ConfigEntries:
|
|||
self.hass, self._hass_config, integration)
|
||||
|
||||
try:
|
||||
integration.get_component()
|
||||
integration.get_platform('config_flow')
|
||||
except ImportError as err:
|
||||
_LOGGER.error(
|
||||
'Error occurred while loading integration %s: %s',
|
||||
'Error occurred loading config flow for integration %s: %s',
|
||||
handler_key, err)
|
||||
raise data_entry_flow.UnknownHandler
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
"""Automatically generated by hassfest.
|
||||
|
||||
To update, run python3 -m hassfest
|
||||
"""
|
||||
|
||||
|
||||
FLOWS = [
|
||||
"ambiclimate",
|
||||
"ambient_station",
|
||||
"axis",
|
||||
"cast",
|
||||
"daikin",
|
||||
"deconz",
|
||||
"dialogflow",
|
||||
"emulated_roku",
|
||||
"esphome",
|
||||
"geofency",
|
||||
"gpslogger",
|
||||
"hangouts",
|
||||
"heos",
|
||||
"homekit_controller",
|
||||
"homematicip_cloud",
|
||||
"hue",
|
||||
"ifttt",
|
||||
"ios",
|
||||
"ipma",
|
||||
"iqvia",
|
||||
"lifx",
|
||||
"locative",
|
||||
"logi_circle",
|
||||
"luftdaten",
|
||||
"mailgun",
|
||||
"mobile_app",
|
||||
"mqtt",
|
||||
"nest",
|
||||
"openuv",
|
||||
"owntracks",
|
||||
"point",
|
||||
"ps4",
|
||||
"rainmachine",
|
||||
"simplisafe",
|
||||
"smartthings",
|
||||
"smhi",
|
||||
"sonos",
|
||||
"tellduslive",
|
||||
"toon",
|
||||
"tplink",
|
||||
"tradfri",
|
||||
"twilio",
|
||||
"unifi",
|
||||
"upnp",
|
||||
"zha",
|
||||
"zone",
|
||||
"zwave"
|
||||
]
|
|
@ -2,9 +2,9 @@
|
|||
import logging
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.util.json import load_json
|
||||
from homeassistant.generated import config_flows
|
||||
from .typing import HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -106,7 +106,7 @@ async def async_get_component_resources(hass: HomeAssistantType,
|
|||
translation_cache = hass.data[TRANSLATION_STRING_CACHE][language]
|
||||
|
||||
# Get the set of components
|
||||
components = hass.config.components | set(config_entries.FLOWS)
|
||||
components = hass.config.components | set(config_flows.FLOWS)
|
||||
|
||||
# Calculate the missing components
|
||||
missing_components = components - set(translation_cache)
|
||||
|
|
6
pylintrc
6
pylintrc
|
@ -1,3 +1,9 @@
|
|||
[MASTER]
|
||||
ignore=tests
|
||||
|
||||
[BASIC]
|
||||
good-names=i,j,k,ex,Run,_,fp
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# Reasons disabled:
|
||||
# locally-disabled - it spams too much
|
||||
|
|
|
@ -3,13 +3,14 @@ import pathlib
|
|||
import sys
|
||||
|
||||
from .model import Integration, Config
|
||||
from . import dependencies, manifest, codeowners, services
|
||||
from . import dependencies, manifest, codeowners, services, config_flow
|
||||
|
||||
PLUGINS = [
|
||||
manifest,
|
||||
dependencies,
|
||||
codeowners,
|
||||
services,
|
||||
config_flow,
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
"""Generate config flow file."""
|
||||
import json
|
||||
from typing import Dict
|
||||
|
||||
from .model import Integration, Config
|
||||
|
||||
BASE = """
|
||||
\"\"\"Automatically generated by hassfest.
|
||||
|
||||
To update, run python3 -m hassfest
|
||||
\"\"\"
|
||||
|
||||
|
||||
FLOWS = {}
|
||||
""".strip()
|
||||
|
||||
|
||||
def validate_integration(integration: Integration):
|
||||
"""Validate we can load config flow without installing requirements."""
|
||||
if not (integration.path / "config_flow.py").is_file():
|
||||
integration.add_error(
|
||||
'config_flow',
|
||||
"Config flows need to be defined in the file config_flow.py")
|
||||
|
||||
# Currently not require being able to load config flow without
|
||||
# installing requirements.
|
||||
# try:
|
||||
# integration.import_pkg('config_flow')
|
||||
# except ImportError as err:
|
||||
# integration.add_error(
|
||||
# 'config_flow',
|
||||
# "Unable to import config flow: {}. Config flows should be able "
|
||||
# "to be imported without installing requirements.".format(err))
|
||||
# return
|
||||
|
||||
# if integration.domain not in config_entries.HANDLERS:
|
||||
# integration.add_error(
|
||||
# 'config_flow',
|
||||
# "Importing the config flow platform did not register a config "
|
||||
# "flow handler.")
|
||||
|
||||
|
||||
def generate_and_validate(integrations: Dict[str, Integration]):
|
||||
"""Validate and generate config flow data."""
|
||||
domains = []
|
||||
|
||||
for domain in sorted(integrations):
|
||||
integration = integrations[domain]
|
||||
|
||||
if not integration.manifest:
|
||||
continue
|
||||
|
||||
config_flow = integration.manifest.get('config_flow')
|
||||
|
||||
if not config_flow:
|
||||
continue
|
||||
|
||||
validate_integration(integration)
|
||||
|
||||
domains.append(domain)
|
||||
|
||||
return BASE.format(json.dumps(domains, indent=4))
|
||||
|
||||
|
||||
def validate(integrations: Dict[str, Integration], config: Config):
|
||||
"""Validate config flow file."""
|
||||
config_flow_path = config.root / 'homeassistant/generated/config_flows.py'
|
||||
config.cache['config_flow'] = content = generate_and_validate(integrations)
|
||||
|
||||
with open(str(config_flow_path), 'r') as fp:
|
||||
if fp.read().strip() != content:
|
||||
config.add_error(
|
||||
"config_flow",
|
||||
"File config_flows.py is not up to date. "
|
||||
"Run python3 -m script.hassfest",
|
||||
fixable=True
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def generate(integrations: Dict[str, Integration], config: Config):
|
||||
"""Generate config flow file."""
|
||||
config_flow_path = config.root / 'homeassistant/generated/config_flows.py'
|
||||
with open(str(config_flow_path), 'w') as fp:
|
||||
fp.write(config.cache['config_flow'] + '\n')
|
|
@ -10,6 +10,7 @@ from .model import Integration
|
|||
MANIFEST_SCHEMA = vol.Schema({
|
||||
vol.Required('domain'): str,
|
||||
vol.Required('name'): str,
|
||||
vol.Optional('config_flow'): bool,
|
||||
vol.Required('documentation'): str,
|
||||
vol.Required('requirements'): [str],
|
||||
vol.Required('dependencies'): [str],
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import json
|
||||
from typing import List, Dict, Any
|
||||
import pathlib
|
||||
import importlib
|
||||
|
||||
import attr
|
||||
|
||||
|
@ -92,3 +93,10 @@ class Integration:
|
|||
return
|
||||
|
||||
self.manifest = manifest
|
||||
|
||||
def import_pkg(self, platform=None):
|
||||
"""Import the Python file."""
|
||||
pkg = "homeassistant.components.{}".format(self.domain)
|
||||
if platform is not None:
|
||||
pkg += ".{}".format(platform)
|
||||
return importlib.import_module(pkg)
|
||||
|
|
|
@ -926,7 +926,7 @@ async def get_system_health_info(hass, domain):
|
|||
def mock_integration(hass, module):
|
||||
"""Mock an integration."""
|
||||
integration = loader.Integration(
|
||||
hass, 'homeassisant.components.{}'.format(module.DOMAIN), None,
|
||||
hass, 'homeassistant.components.{}'.format(module.DOMAIN), None,
|
||||
module.mock_manifest())
|
||||
|
||||
_LOGGER.info("Adding mock integration: %s", module.DOMAIN)
|
||||
|
|
|
@ -12,9 +12,11 @@ from homeassistant.config_entries import HANDLERS
|
|||
from homeassistant.core import callback
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components.config import config_entries
|
||||
from homeassistant.generated import config_flows
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry, MockModule, mock_coro_func, mock_integration)
|
||||
MockConfigEntry, MockModule, mock_coro_func, mock_integration,
|
||||
mock_entity_platform)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
@ -121,7 +123,7 @@ async def test_remove_entry_unauth(hass, client, hass_admin_user):
|
|||
@asyncio.coroutine
|
||||
def test_available_flows(hass, client):
|
||||
"""Test querying the available flows."""
|
||||
with patch.object(core_ce, 'FLOWS', ['hello', 'world']):
|
||||
with patch.object(config_flows, 'FLOWS', ['hello', 'world']):
|
||||
resp = yield from client.get(
|
||||
'/api/config/config_entries/flow_handlers')
|
||||
assert resp.status == 200
|
||||
|
@ -137,6 +139,8 @@ def test_available_flows(hass, client):
|
|||
@asyncio.coroutine
|
||||
def test_initialize_flow(hass, client):
|
||||
"""Test we can initialize a flow."""
|
||||
mock_entity_platform(hass, 'config_flow.test', None)
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
@asyncio.coroutine
|
||||
def async_step_user(self, user_input=None):
|
||||
|
@ -221,6 +225,8 @@ async def test_initialize_flow_unauth(hass, client, hass_admin_user):
|
|||
@asyncio.coroutine
|
||||
def test_abort(hass, client):
|
||||
"""Test a flow that aborts."""
|
||||
mock_entity_platform(hass, 'config_flow.test', None)
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
@asyncio.coroutine
|
||||
def async_step_user(self, user_input=None):
|
||||
|
@ -244,6 +250,8 @@ def test_abort(hass, client):
|
|||
@asyncio.coroutine
|
||||
def test_create_account(hass, client):
|
||||
"""Test a flow that creates an account."""
|
||||
mock_entity_platform(hass, 'config_flow.test', None)
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule('test', async_setup_entry=mock_coro_func(True)))
|
||||
|
@ -286,6 +294,7 @@ def test_two_step_flow(hass, client):
|
|||
mock_integration(
|
||||
hass,
|
||||
MockModule('test', async_setup_entry=mock_coro_func(True)))
|
||||
mock_entity_platform(hass, 'config_flow.test', None)
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
VERSION = 1
|
||||
|
@ -352,6 +361,7 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user):
|
|||
mock_integration(
|
||||
hass,
|
||||
MockModule('test', async_setup_entry=mock_coro_func(True)))
|
||||
mock_entity_platform(hass, 'config_flow.test', None)
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
VERSION = 1
|
||||
|
@ -402,6 +412,8 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user):
|
|||
@asyncio.coroutine
|
||||
def test_get_progress_index(hass, client):
|
||||
"""Test querying for the flows that are in progress."""
|
||||
mock_entity_platform(hass, 'config_flow.test', None)
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
VERSION = 5
|
||||
|
||||
|
@ -441,6 +453,8 @@ async def test_get_progress_index_unauth(hass, client, hass_admin_user):
|
|||
@asyncio.coroutine
|
||||
def test_get_progress_flow(hass, client):
|
||||
"""Test we can query the API for same result as we get from init a flow."""
|
||||
mock_entity_platform(hass, 'config_flow.test', None)
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
@asyncio.coroutine
|
||||
def async_step_user(self, user_input=None):
|
||||
|
@ -474,6 +488,8 @@ def test_get_progress_flow(hass, client):
|
|||
|
||||
async def test_get_progress_flow_unauth(hass, client, hass_admin_user):
|
||||
"""Test we can can't query the API for result of flow."""
|
||||
mock_entity_platform(hass, 'config_flow.test', None)
|
||||
|
||||
class TestFlow(core_ce.ConfigFlow):
|
||||
async def async_step_user(self, user_input=None):
|
||||
schema = OrderedDict()
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue