Add spider config flow (#36001)
This commit is contained in:
parent
bbf31b1101
commit
ab512a1273
12 changed files with 314 additions and 49 deletions
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
79
homeassistant/components/spider/config_flow.py
Normal file
79
homeassistant/components/spider/config_flow.py
Normal 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)
|
6
homeassistant/components/spider/const.py
Normal file
6
homeassistant/components/spider/const.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
"""Constants for the Spider integration."""
|
||||
|
||||
DOMAIN = "spider"
|
||||
DEFAULT_SCAN_INTERVAL = 300
|
||||
|
||||
PLATFORMS = ["climate", "switch"]
|
|
@ -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
|
||||
}
|
||||
|
|
20
homeassistant/components/spider/strings.json
Normal file
20
homeassistant/components/spider/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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):
|
||||
|
|
20
homeassistant/components/spider/translations/en.json
Normal file
20
homeassistant/components/spider/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -158,6 +158,7 @@ FLOWS = [
|
|||
"songpal",
|
||||
"sonos",
|
||||
"speedtestdotnet",
|
||||
"spider",
|
||||
"spotify",
|
||||
"squeezebox",
|
||||
"starline",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
1
tests/components/spider/__init__.py
Normal file
1
tests/components/spider/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Spider component."""
|
100
tests/components/spider/test_config_flow.py
Normal file
100
tests/components/spider/test_config_flow.py
Normal 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"
|
Loading…
Add table
Add a link
Reference in a new issue