mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Add spider config flow (#36001)
This commit is contained in:
parent
bbf31b1101
commit
ab512a1273
@ -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
|
||||||
|
@ -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):
|
||||||
|
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",
|
"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
|
||||||
}
|
}
|
||||||
|
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 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):
|
||||||
|
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",
|
"songpal",
|
||||||
"sonos",
|
"sonos",
|
||||||
"speedtestdotnet",
|
"speedtestdotnet",
|
||||||
|
"spider",
|
||||||
"spotify",
|
"spotify",
|
||||||
"squeezebox",
|
"squeezebox",
|
||||||
"starline",
|
"starline",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
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…
x
Reference in New Issue
Block a user