Add spider config flow (#36001)

This commit is contained in:
Peter Nijssen 2020-08-04 22:37:20 +02:00 committed by GitHub
parent bbf31b1101
commit ab512a1273
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 314 additions and 49 deletions

View File

@ -1,29 +1,27 @@
"""Support for Spider Smart devices.""" """Support for Spider Smart devices."""
from datetime import timedelta import asyncio
import logging import logging
from spiderpy.spiderapi import SpiderApi, UnauthorizedException from spiderpy.spiderapi import SpiderApi, UnauthorizedException
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "spider"
SPIDER_COMPONENTS = ["climate", "switch"]
SCAN_INTERVAL = timedelta(seconds=120)
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{ {
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.time_period,
} }
) )
}, },
@ -31,27 +29,66 @@ CONFIG_SCHEMA = vol.Schema(
) )
def setup(hass, config): def _spider_startup_wrapper(entry):
"""Set up Spider Component.""" """Startup wrapper for spider."""
api = SpiderApi(
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
entry.data[CONF_SCAN_INTERVAL],
)
return api
username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
refresh_rate = config[DOMAIN][CONF_SCAN_INTERVAL]
try: async def async_setup(hass, config):
api = SpiderApi(username, password, refresh_rate.total_seconds()) """Set up a config entry."""
hass.data[DOMAIN] = {}
hass.data[DOMAIN] = { if DOMAIN not in config:
"controller": api,
"thermostats": api.get_thermostats(),
"power_plugs": api.get_power_plugs(),
}
for component in SPIDER_COMPONENTS:
load_platform(hass, component, DOMAIN, {}, config)
_LOGGER.debug("Connection with Spider API succeeded")
return True return True
conf = config[DOMAIN]
if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
return True
async def async_setup_entry(hass, entry):
"""Set up Spider via config entry."""
try:
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
_spider_startup_wrapper, entry
)
except UnauthorizedException: except UnauthorizedException:
_LOGGER.error("Can't connect to the Spider API") _LOGGER.error("Can't connect to the Spider API")
return False return False
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass, entry):
"""Unload Spider entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if not unload_ok:
return False
hass.data[DOMAIN].pop(entry.entry_id)
return True

View File

@ -12,7 +12,7 @@ from homeassistant.components.climate.const import (
) )
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from . import DOMAIN as SPIDER_DOMAIN from .const import DOMAIN
SUPPORT_FAN = ["Auto", "Low", "Medium", "High", "Boost 10", "Boost 20", "Boost 30"] SUPPORT_FAN = ["Auto", "Low", "Medium", "High", "Boost 10", "Boost 20", "Boost 30"]
@ -29,16 +29,13 @@ SPIDER_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_SPIDER.items()}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config, async_add_entities):
"""Set up the Spider thermostat.""" """Initialize a Spider thermostat."""
if discovery_info is None: api = hass.data[DOMAIN][config.entry_id]
return
devices = [ entities = [SpiderThermostat(api, entity) for entity in api.get_thermostats()]
SpiderThermostat(hass.data[SPIDER_DOMAIN]["controller"], device)
for device in hass.data[SPIDER_DOMAIN]["thermostats"] async_add_entities(entities)
]
add_entities(devices, True)
class SpiderThermostat(ClimateEntity): class SpiderThermostat(ClimateEntity):

View File

@ -0,0 +1,79 @@
"""Config flow for Spider."""
import logging
from spiderpy.spiderapi import SpiderApi, SpiderApiException, UnauthorizedException
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA_USER = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
RESULT_AUTH_FAILED = "auth_failed"
RESULT_CONN_ERROR = "conn_error"
RESULT_SUCCESS = "success"
class SpiderConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Spider config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize the Spider flow."""
self.data = {
CONF_USERNAME: "",
CONF_PASSWORD: "",
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
}
def _try_connect(self):
"""Try to connect and check auth."""
try:
SpiderApi(
self.data[CONF_USERNAME],
self.data[CONF_PASSWORD],
self.data[CONF_SCAN_INTERVAL],
)
except SpiderApiException:
return RESULT_CONN_ERROR
except UnauthorizedException:
return RESULT_AUTH_FAILED
return RESULT_SUCCESS
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors = {}
if user_input is not None:
self.data[CONF_USERNAME] = user_input["username"]
self.data[CONF_PASSWORD] = user_input["password"]
result = await self.hass.async_add_executor_job(self._try_connect)
if result == RESULT_SUCCESS:
return self.async_create_entry(title=DOMAIN, data=self.data,)
if result != RESULT_AUTH_FAILED:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_abort(reason=result)
errors["base"] = "invalid_auth"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors,
)
async def async_step_import(self, import_data):
"""Import spider config from configuration.yaml."""
return await self.async_step_user(import_data)

View File

@ -0,0 +1,6 @@
"""Constants for the Spider integration."""
DOMAIN = "spider"
DEFAULT_SCAN_INTERVAL = 300
PLATFORMS = ["climate", "switch"]

View File

@ -2,6 +2,11 @@
"domain": "spider", "domain": "spider",
"name": "Itho Daalderop Spider", "name": "Itho Daalderop Spider",
"documentation": "https://www.home-assistant.io/integrations/spider", "documentation": "https://www.home-assistant.io/integrations/spider",
"requirements": ["spiderpy==1.3.1"], "requirements": [
"codeowners": ["@peternijssen"] "spiderpy==1.3.1"
],
"codeowners": [
"@peternijssen"
],
"config_flow": true
} }

View File

@ -0,0 +1,20 @@
{
"config": {
"step": {
"user": {
"title": "Sign-in with mijn.ithodaalderop.nl account",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
}
}

View File

@ -3,22 +3,18 @@ import logging
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from . import DOMAIN as SPIDER_DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config, async_add_entities):
"""Set up the Spider thermostat.""" """Initialize a Spider thermostat."""
if discovery_info is None: api = hass.data[DOMAIN][config.entry_id]
return
devices = [ entities = [SpiderPowerPlug(api, entity) for entity in api.get_power_plugs()]
SpiderPowerPlug(hass.data[SPIDER_DOMAIN]["controller"], device)
for device in hass.data[SPIDER_DOMAIN]["power_plugs"]
]
add_entities(devices, True) async_add_entities(entities)
class SpiderPowerPlug(SwitchEntity): class SpiderPowerPlug(SwitchEntity):

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"password": "Password",
"username": "Username"
},
"title": "Sign-in with your mijn.ithodaalderop.nl account"
}
}
}
}

View File

@ -158,6 +158,7 @@ FLOWS = [
"songpal", "songpal",
"sonos", "sonos",
"speedtestdotnet", "speedtestdotnet",
"spider",
"spotify", "spotify",
"squeezebox", "squeezebox",
"starline", "starline",

View File

@ -904,6 +904,9 @@ speak2mary==1.4.0
# homeassistant.components.speedtestdotnet # homeassistant.components.speedtestdotnet
speedtest-cli==2.1.2 speedtest-cli==2.1.2
# homeassistant.components.spider
spiderpy==1.3.1
# homeassistant.components.spotify # homeassistant.components.spotify
spotipy==2.12.0 spotipy==2.12.0

View File

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

View File

@ -0,0 +1,100 @@
"""Tests for the Spider config flow."""
import pytest
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.spider.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from tests.async_mock import Mock, patch
from tests.common import MockConfigEntry
USERNAME = "spider-username"
PASSWORD = "spider-password"
SPIDER_USER_DATA = {
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
}
@pytest.fixture(name="spider")
def spider_fixture() -> Mock:
"""Patch libraries."""
with patch("homeassistant.components.spider.config_flow.SpiderApi") as spider:
yield spider
async def test_user(hass, spider):
"""Test user config."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
with patch(
"homeassistant.components.spider.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.spider.async_setup_entry", return_value=True
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=SPIDER_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == DOMAIN
assert result["data"][CONF_USERNAME] == USERNAME
assert result["data"][CONF_PASSWORD] == PASSWORD
assert not result["result"].unique_id
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_import(hass, spider):
"""Test import step."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.spider.async_setup", return_value=True,
) as mock_setup, patch(
"homeassistant.components.spider.async_setup_entry", return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=SPIDER_USER_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == DOMAIN
assert result["data"][CONF_USERNAME] == USERNAME
assert result["data"][CONF_PASSWORD] == PASSWORD
assert not result["result"].unique_id
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_abort_if_already_setup(hass, spider):
"""Test we abort if Spider is already setup."""
MockConfigEntry(domain=DOMAIN, data=SPIDER_USER_DATA).add_to_hass(hass)
# Should fail, config exist (import)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SPIDER_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"
# Should fail, config exist (flow)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=SPIDER_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"