mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add zeroconf detection to devolo Home Control (#47934)
Co-authored-by: Markus Bong <2Fake1987@gmail.com>
This commit is contained in:
parent
34b258e812
commit
77372d9094
@ -12,14 +12,20 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTAN
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .const import CONF_MYDEVOLO, DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS
|
from .const import (
|
||||||
|
CONF_MYDEVOLO,
|
||||||
|
DEFAULT_MYDEVOLO,
|
||||||
|
DOMAIN,
|
||||||
|
GATEWAY_SERIAL_PATTERN,
|
||||||
|
PLATFORMS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up the devolo account from a config entry."""
|
"""Set up the devolo account from a config entry."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
mydevolo = _mydevolo(entry.data)
|
mydevolo = configure_mydevolo(entry.data)
|
||||||
|
|
||||||
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
|
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
|
||||||
|
|
||||||
@ -92,10 +98,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return unload
|
return unload
|
||||||
|
|
||||||
|
|
||||||
def _mydevolo(conf: dict) -> Mydevolo:
|
def configure_mydevolo(conf: dict) -> Mydevolo:
|
||||||
"""Configure mydevolo."""
|
"""Configure mydevolo."""
|
||||||
mydevolo = Mydevolo()
|
mydevolo = Mydevolo()
|
||||||
mydevolo.user = conf[CONF_USERNAME]
|
mydevolo.user = conf[CONF_USERNAME]
|
||||||
mydevolo.password = conf[CONF_PASSWORD]
|
mydevolo.password = conf[CONF_PASSWORD]
|
||||||
mydevolo.url = conf[CONF_MYDEVOLO]
|
mydevolo.url = conf.get(CONF_MYDEVOLO, DEFAULT_MYDEVOLO)
|
||||||
return mydevolo
|
return mydevolo
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
"""Config flow to configure the devolo home control integration."""
|
"""Config flow to configure the devolo home control integration."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from devolo_home_control_api.mydevolo import Mydevolo
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN
|
from . import configure_mydevolo
|
||||||
|
from .const import ( # pylint:disable=unused-import
|
||||||
|
CONF_MYDEVOLO,
|
||||||
|
DEFAULT_MYDEVOLO,
|
||||||
|
DOMAIN,
|
||||||
|
SUPPORTED_MODEL_TYPES,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,22 +35,30 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Handle a flow initiated by the user."""
|
"""Handle a flow initiated by the user."""
|
||||||
if self.show_advanced_options:
|
if self.show_advanced_options:
|
||||||
self.data_schema = {
|
self.data_schema[
|
||||||
vol.Required(CONF_USERNAME): str,
|
vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO)
|
||||||
vol.Required(CONF_PASSWORD): str,
|
] = str
|
||||||
vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO): str,
|
|
||||||
}
|
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self._show_form(user_input)
|
return self._show_form(user_input)
|
||||||
user = user_input[CONF_USERNAME]
|
return await self._connect_mydevolo(user_input)
|
||||||
password = user_input[CONF_PASSWORD]
|
|
||||||
mydevolo = Mydevolo()
|
async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
|
||||||
mydevolo.user = user
|
"""Handle zeroconf discovery."""
|
||||||
mydevolo.password = password
|
# Check if it is a gateway
|
||||||
if self.show_advanced_options:
|
if discovery_info.get("properties", {}).get("MT") in SUPPORTED_MODEL_TYPES:
|
||||||
mydevolo.url = user_input[CONF_MYDEVOLO]
|
await self._async_handle_discovery_without_unique_id()
|
||||||
else:
|
return await self.async_step_zeroconf_confirm()
|
||||||
mydevolo.url = DEFAULT_MYDEVOLO
|
return self.async_abort(reason="Not a devolo Home Control gateway.")
|
||||||
|
|
||||||
|
async def async_step_zeroconf_confirm(self, user_input=None):
|
||||||
|
"""Handle a flow initiated by zeroconf."""
|
||||||
|
if user_input is None:
|
||||||
|
return self._show_form(step_id="zeroconf_confirm")
|
||||||
|
return await self._connect_mydevolo(user_input)
|
||||||
|
|
||||||
|
async def _connect_mydevolo(self, user_input):
|
||||||
|
"""Connect to mydevolo."""
|
||||||
|
mydevolo = configure_mydevolo(conf=user_input)
|
||||||
credentials_valid = await self.hass.async_add_executor_job(
|
credentials_valid = await self.hass.async_add_executor_job(
|
||||||
mydevolo.credentials_valid
|
mydevolo.credentials_valid
|
||||||
)
|
)
|
||||||
@ -58,17 +72,17 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title="devolo Home Control",
|
title="devolo Home Control",
|
||||||
data={
|
data={
|
||||||
CONF_PASSWORD: password,
|
CONF_PASSWORD: mydevolo.password,
|
||||||
CONF_USERNAME: user,
|
CONF_USERNAME: mydevolo.user,
|
||||||
CONF_MYDEVOLO: mydevolo.url,
|
CONF_MYDEVOLO: mydevolo.url,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _show_form(self, errors=None):
|
def _show_form(self, errors=None, step_id="user"):
|
||||||
"""Show the form to the user."""
|
"""Show the form to the user."""
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id=step_id,
|
||||||
data_schema=vol.Schema(self.data_schema),
|
data_schema=vol.Schema(self.data_schema),
|
||||||
errors=errors if errors else {},
|
errors=errors if errors else {},
|
||||||
)
|
)
|
||||||
|
@ -6,3 +6,4 @@ DEFAULT_MYDEVOLO = "https://www.mydevolo.com"
|
|||||||
PLATFORMS = ["binary_sensor", "climate", "cover", "light", "sensor", "switch"]
|
PLATFORMS = ["binary_sensor", "climate", "cover", "light", "sensor", "switch"]
|
||||||
CONF_MYDEVOLO = "mydevolo_url"
|
CONF_MYDEVOLO = "mydevolo_url"
|
||||||
GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}")
|
GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}")
|
||||||
|
SUPPORTED_MODEL_TYPES = ["2600", "2601"]
|
||||||
|
@ -7,5 +7,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"codeowners": ["@2Fake", "@Shutgun"],
|
"codeowners": ["@2Fake", "@Shutgun"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push",
|
||||||
|
"zeroconf": ["_dvl-deviceapi._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,17 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::email%] / devolo ID",
|
"username": "[%key:common::config_flow::data::email%] / devolo ID",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]",
|
"mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]"
|
||||||
"home_control_url": "Home Control [%key:common::config_flow::data::url%]"
|
}
|
||||||
|
},
|
||||||
|
"zeroconf_confirm": {
|
||||||
|
"data": {
|
||||||
|
"username": "[%key:common::config_flow::data::email%] / devolo ID",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,13 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"home_control_url": "Home Control URL",
|
"mydevolo_url": "mydevolo URL",
|
||||||
|
"password": "Password",
|
||||||
|
"username": "Email / devolo ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zeroconf_confirm": {
|
||||||
|
"data": {
|
||||||
"mydevolo_url": "mydevolo URL",
|
"mydevolo_url": "mydevolo URL",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"username": "Email / devolo ID"
|
"username": "Email / devolo ID"
|
||||||
|
@ -49,6 +49,11 @@ ZEROCONF = {
|
|||||||
"domain": "daikin"
|
"domain": "daikin"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"_dvl-deviceapi._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "devolo_home_control"
|
||||||
|
}
|
||||||
|
],
|
||||||
"_elg._tcp.local.": [
|
"_elg._tcp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "elgato"
|
"domain": "elgato"
|
||||||
|
22
tests/components/devolo_home_control/const.py
Normal file
22
tests/components/devolo_home_control/const.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Constants used for mocking data."""
|
||||||
|
|
||||||
|
DISCOVERY_INFO = {
|
||||||
|
"host": "192.168.0.1",
|
||||||
|
"port": 14791,
|
||||||
|
"hostname": "test.local.",
|
||||||
|
"type": "_dvl-deviceapi._tcp.local.",
|
||||||
|
"name": "dvl-deviceapi",
|
||||||
|
"properties": {
|
||||||
|
"Path": "/deviceapi",
|
||||||
|
"Version": "v0",
|
||||||
|
"Features": "",
|
||||||
|
"MT": "2600",
|
||||||
|
"SN": "1234567890",
|
||||||
|
"FirmwareVersion": "8.90.4",
|
||||||
|
"PlcMacAddress": "AA:BB:CC:DD:EE:FF",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
DISCOVERY_INFO_WRONG_DEVOLO_DEVICE = {"properties": {"MT": "2700"}}
|
||||||
|
|
||||||
|
DISCOVERY_INFO_WRONG_DEVICE = {"properties": {"Features": ""}}
|
@ -4,9 +4,15 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow, setup
|
from homeassistant import config_entries, data_entry_flow, setup
|
||||||
from homeassistant.components.devolo_home_control.const import DOMAIN
|
from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_USER
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
DISCOVERY_INFO,
|
||||||
|
DISCOVERY_INFO_WRONG_DEVICE,
|
||||||
|
DISCOVERY_INFO_WRONG_DEVOLO_DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
@ -19,28 +25,7 @@ async def test_form(hass):
|
|||||||
assert result["type"] == "form"
|
assert result["type"] == "form"
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
with patch(
|
await _setup(hass, result)
|
||||||
"homeassistant.components.devolo_home_control.async_setup_entry",
|
|
||||||
return_value=True,
|
|
||||||
) as mock_setup_entry, patch(
|
|
||||||
"homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid",
|
|
||||||
return_value="123456",
|
|
||||||
):
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{"username": "test-username", "password": "test-password"},
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert result2["type"] == "create_entry"
|
|
||||||
assert result2["title"] == "devolo Home Control"
|
|
||||||
assert result2["data"] == {
|
|
||||||
"username": "test-username",
|
|
||||||
"password": "test-password",
|
|
||||||
"mydevolo_url": "https://www.mydevolo.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.credentials_invalid
|
@pytest.mark.credentials_invalid
|
||||||
@ -64,7 +49,7 @@ async def test_form_invalid_credentials(hass):
|
|||||||
async def test_form_already_configured(hass):
|
async def test_form_already_configured(hass):
|
||||||
"""Test if we get the error message on already configured."""
|
"""Test if we get the error message on already configured."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid",
|
"homeassistant.components.devolo_home_control.Mydevolo.uuid",
|
||||||
return_value="123456",
|
return_value="123456",
|
||||||
):
|
):
|
||||||
MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}).add_to_hass(hass)
|
MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}).add_to_hass(hass)
|
||||||
@ -89,7 +74,7 @@ async def test_form_advanced_options(hass):
|
|||||||
"homeassistant.components.devolo_home_control.async_setup_entry",
|
"homeassistant.components.devolo_home_control.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup_entry, patch(
|
) as mock_setup_entry, patch(
|
||||||
"homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid",
|
"homeassistant.components.devolo_home_control.Mydevolo.uuid",
|
||||||
return_value="123456",
|
return_value="123456",
|
||||||
):
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
@ -111,3 +96,64 @@ async def test_form_advanced_options(hass):
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_zeroconf_form(hass):
|
||||||
|
"""Test that the zeroconf confirmation form is served."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data=DISCOVERY_INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["step_id"] == "zeroconf_confirm"
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
|
||||||
|
await _setup(hass, result)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_wrong_device(hass):
|
||||||
|
"""Test that the zeroconf ignores wrong devices."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data=DISCOVERY_INFO_WRONG_DEVOLO_DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["reason"] == "Not a devolo Home Control gateway."
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data=DISCOVERY_INFO_WRONG_DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["reason"] == "Not a devolo Home Control gateway."
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
|
||||||
|
|
||||||
|
async def _setup(hass, result):
|
||||||
|
"""Finish configuration steps."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.devolo_home_control.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry, patch(
|
||||||
|
"homeassistant.components.devolo_home_control.Mydevolo.uuid",
|
||||||
|
return_value="123456",
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"username": "test-username", "password": "test-password"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == "devolo Home Control"
|
||||||
|
assert result2["data"] == {
|
||||||
|
"username": "test-username",
|
||||||
|
"password": "test-password",
|
||||||
|
"mydevolo_url": DEFAULT_MYDEVOLO,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user