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."""
from datetime import timedelta
import asyncio
import logging
from spiderpy.spiderapi import SpiderApi, UnauthorizedException
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
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__)
DOMAIN = "spider"
SPIDER_COMPONENTS = ["climate", "switch"]
SCAN_INTERVAL = timedelta(seconds=120)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_PASSWORD): 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):
"""Set up Spider Component."""
def _spider_startup_wrapper(entry):
"""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:
api = SpiderApi(username, password, refresh_rate.total_seconds())
hass.data[DOMAIN] = {
"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")
async def async_setup(hass, config):
"""Set up a config entry."""
hass.data[DOMAIN] = {}
if DOMAIN not in config:
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:
_LOGGER.error("Can't connect to the Spider API")
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 . import DOMAIN as SPIDER_DOMAIN
from .const import DOMAIN
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__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Spider thermostat."""
if discovery_info is None:
return
async def async_setup_entry(hass, config, async_add_entities):
"""Initialize a Spider thermostat."""
api = hass.data[DOMAIN][config.entry_id]
devices = [
SpiderThermostat(hass.data[SPIDER_DOMAIN]["controller"], device)
for device in hass.data[SPIDER_DOMAIN]["thermostats"]
]
add_entities(devices, True)
entities = [SpiderThermostat(api, entity) for entity in api.get_thermostats()]
async_add_entities(entities)
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",
"name": "Itho Daalderop Spider",
"documentation": "https://www.home-assistant.io/integrations/spider",
"requirements": ["spiderpy==1.3.1"],
"codeowners": ["@peternijssen"]
"requirements": [
"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 . import DOMAIN as SPIDER_DOMAIN
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Spider thermostat."""
if discovery_info is None:
return
async def async_setup_entry(hass, config, async_add_entities):
"""Initialize a Spider thermostat."""
api = hass.data[DOMAIN][config.entry_id]
devices = [
SpiderPowerPlug(hass.data[SPIDER_DOMAIN]["controller"], device)
for device in hass.data[SPIDER_DOMAIN]["power_plugs"]
]
entities = [SpiderPowerPlug(api, entity) for entity in api.get_power_plugs()]
add_entities(devices, True)
async_add_entities(entities)
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",
"sonos",
"speedtestdotnet",
"spider",
"spotify",
"squeezebox",
"starline",

View file

@ -904,6 +904,9 @@ speak2mary==1.4.0
# homeassistant.components.speedtestdotnet
speedtest-cli==2.1.2
# homeassistant.components.spider
spiderpy==1.3.1
# homeassistant.components.spotify
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"