Add config flow for somfy_mylink (#44977)

* Add config flow for somfy_mylink

* fix typo
This commit is contained in:
J. Nick Koston 2021-01-10 09:24:22 -10:00 committed by GitHub
parent b450d4c135
commit 4b54694c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 920 additions and 54 deletions

View File

@ -838,7 +838,8 @@ omit =
homeassistant/components/soma/cover.py
homeassistant/components/soma/sensor.py
homeassistant/components/somfy/*
homeassistant/components/somfy_mylink/*
homeassistant/components/somfy_mylink/__init__.py
homeassistant/components/somfy_mylink/cover.py
homeassistant/components/sonos/*
homeassistant/components/sony_projector/switch.py
homeassistant/components/spc/*

View File

@ -421,6 +421,7 @@ homeassistant/components/solarlog/* @Ernst79
homeassistant/components/solax/* @squishykid
homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne
homeassistant/components/somfy_mylink/* @bdraco
homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti @shenxn
homeassistant/components/sonos/* @cgtobi

View File

@ -1,18 +1,33 @@
"""Component for the Somfy MyLink device supporting the Synergy API."""
import asyncio
import logging
from somfy_mylink_synergy import SomfyMyLinkSynergy
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
CONF_ENTITY_CONFIG = "entity_config"
CONF_SYSTEM_ID = "system_id"
CONF_REVERSE = "reverse"
CONF_DEFAULT_REVERSE = "default_reverse"
DATA_SOMFY_MYLINK = "somfy_mylink_data"
DOMAIN = "somfy_mylink"
SOMFY_MYLINK_COMPONENTS = ["cover"]
from .const import (
CONF_DEFAULT_REVERSE,
CONF_ENTITY_CONFIG,
CONF_REVERSE,
CONF_SYSTEM_ID,
DATA_SOMFY_MYLINK,
DEFAULT_PORT,
DOMAIN,
MYLINK_ENTITY_IDS,
MYLINK_STATUS,
SOMFY_MYLINK_COMPONENTS,
)
CONFIG_OPTIONS = (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG)
UNDO_UPDATE_LISTENER = "undo_update_listener"
_LOGGER = logging.getLogger(__name__)
def validate_entity_config(values):
@ -34,7 +49,7 @@ CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_SYSTEM_ID): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=44100): cv.port,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_DEFAULT_REVERSE, default=False): cv.boolean,
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
}
@ -47,15 +62,94 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Set up the MyLink platform."""
host = config[DOMAIN][CONF_HOST]
port = config[DOMAIN][CONF_PORT]
system_id = config[DOMAIN][CONF_SYSTEM_ID]
entity_config = config[DOMAIN][CONF_ENTITY_CONFIG]
entity_config[CONF_DEFAULT_REVERSE] = config[DOMAIN][CONF_DEFAULT_REVERSE]
somfy_mylink = SomfyMyLinkSynergy(system_id, host, port)
hass.data[DATA_SOMFY_MYLINK] = somfy_mylink
conf = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})
if not conf:
return True
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: HomeAssistant, entry: ConfigEntry):
"""Set up Somfy MyLink from a config entry."""
_async_import_options_from_data_if_missing(hass, entry)
config = entry.data
somfy_mylink = SomfyMyLinkSynergy(
config[CONF_SYSTEM_ID], config[CONF_HOST], config[CONF_PORT]
)
try:
mylink_status = await somfy_mylink.status_info()
except asyncio.TimeoutError as ex:
raise ConfigEntryNotReady(
"Unable to connect to the Somfy MyLink device, please check your settings"
) from ex
if "error" in mylink_status:
_LOGGER.error(
"mylink failed to setup because of an error: %s",
mylink_status.get("error", {}).get("message"),
)
return False
undo_listener = entry.add_update_listener(_async_update_listener)
hass.data[DOMAIN][entry.entry_id] = {
DATA_SOMFY_MYLINK: somfy_mylink,
MYLINK_STATUS: mylink_status,
MYLINK_ENTITY_IDS: [],
UNDO_UPDATE_LISTENER: undo_listener,
}
for component in SOMFY_MYLINK_COMPONENTS:
hass.async_create_task(
async_load_platform(hass, component, DOMAIN, entity_config, config)
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
@callback
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
options = dict(entry.options)
data = dict(entry.data)
modified = False
for importable_option in CONFIG_OPTIONS:
if importable_option not in options and importable_option in data:
options[importable_option] = data.pop(importable_option)
modified = True
if modified:
hass.config_entries.async_update_entry(entry, data=data, options=options)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in SOMFY_MYLINK_COMPONENTS
]
)
)
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,203 @@
"""Config flow for Somfy MyLink integration."""
import asyncio
import logging
from somfy_mylink_synergy import SomfyMyLinkSynergy
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_ENTITY_ID, CONF_HOST, CONF_PORT
from homeassistant.core import callback
from .const import (
CONF_DEFAULT_REVERSE,
CONF_ENTITY_CONFIG,
CONF_REVERSE,
CONF_SYSTEM_ID,
DEFAULT_CONF_DEFAULT_REVERSE,
DEFAULT_PORT,
MYLINK_ENTITY_IDS,
)
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
ENTITY_CONFIG_VERSION = "entity_config_version"
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_SYSTEM_ID): int,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
}
)
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
somfy_mylink = SomfyMyLinkSynergy(
data[CONF_SYSTEM_ID], data[CONF_HOST], data[CONF_PORT]
)
try:
status_info = await somfy_mylink.status_info()
except asyncio.TimeoutError as ex:
raise CannotConnect from ex
if not status_info or "error" in status_info:
raise InvalidAuth
return {"title": f"MyLink {data[CONF_HOST]}"}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Somfy MyLink."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
if self._host_already_configured(user_input[CONF_HOST]):
return self.async_abort(reason="already_configured")
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_import(self, user_input):
"""Handle import."""
if self._host_already_configured(user_input[CONF_HOST]):
return self.async_abort(reason="already_configured")
return await self.async_step_user(user_input)
def _host_already_configured(self, host):
"""See if we already have an entry matching the host."""
for entry in self._async_current_entries():
if entry.data[CONF_HOST] == host:
return True
return False
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for somfy_mylink."""
def __init__(self, config_entry: config_entries.ConfigEntry):
"""Initialize options flow."""
self.config_entry = config_entry
self.options = config_entry.options.copy()
self._entity_id = None
async def async_step_init(self, user_input=None):
"""Handle options flow."""
if self.config_entry.state != config_entries.ENTRY_STATE_LOADED:
_LOGGER.error("MyLink must be connected to manage device options")
return self.async_abort(reason="cannot_connect")
if user_input is not None:
self.options[CONF_DEFAULT_REVERSE] = user_input[CONF_DEFAULT_REVERSE]
entity_id = user_input.get(CONF_ENTITY_ID)
if entity_id:
return await self.async_step_entity_config(None, entity_id)
return self.async_create_entry(title="", data=self.options)
data_schema = vol.Schema(
{
vol.Required(
CONF_DEFAULT_REVERSE,
default=self.options.get(
CONF_DEFAULT_REVERSE, DEFAULT_CONF_DEFAULT_REVERSE
),
): bool
}
)
data = self.hass.data[DOMAIN][self.config_entry.entry_id]
mylink_entity_ids = data[MYLINK_ENTITY_IDS]
if mylink_entity_ids:
entity_dict = {None: None}
for entity_id in mylink_entity_ids:
name = entity_id
state = self.hass.states.get(entity_id)
if state:
name = state.attributes.get(ATTR_FRIENDLY_NAME, entity_id)
entity_dict[entity_id] = f"{name} ({entity_id})"
data_schema = data_schema.extend(
{vol.Optional(CONF_ENTITY_ID): vol.In(entity_dict)}
)
return self.async_show_form(step_id="init", data_schema=data_schema, errors={})
async def async_step_entity_config(self, user_input=None, entity_id=None):
"""Handle options flow for entity."""
entities_config = self.options.setdefault(CONF_ENTITY_CONFIG, {})
if user_input is not None:
entity_config = entities_config.setdefault(self._entity_id, {})
if entity_config.get(CONF_REVERSE) != user_input[CONF_REVERSE]:
entity_config[CONF_REVERSE] = user_input[CONF_REVERSE]
# If we do not modify a top level key
# the entity config will never be written
self.options.setdefault(ENTITY_CONFIG_VERSION, 0)
self.options[ENTITY_CONFIG_VERSION] += 1
return await self.async_step_init()
self._entity_id = entity_id
default_reverse = self.options.get(CONF_DEFAULT_REVERSE, False)
entity_config = entities_config.get(entity_id, {})
return self.async_show_form(
step_id="entity_config",
data_schema=vol.Schema(
{
vol.Optional(
CONF_REVERSE,
default=entity_config.get(CONF_REVERSE, default_reverse),
): bool
}
),
description_placeholders={
CONF_ENTITY_ID: entity_id,
},
errors={},
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -0,0 +1,14 @@
"""Component for the Somfy MyLink device supporting the Synergy API."""
CONF_ENTITY_CONFIG = "entity_config"
CONF_SYSTEM_ID = "system_id"
CONF_REVERSE = "reverse"
CONF_DEFAULT_REVERSE = "default_reverse"
DEFAULT_CONF_DEFAULT_REVERSE = False
DATA_SOMFY_MYLINK = "somfy_mylink_data"
MYLINK_STATUS = "mylink_status"
MYLINK_ENTITY_IDS = "mylink_entity_ids"
DOMAIN = "somfy_mylink"
SOMFY_MYLINK_COMPONENTS = ["cover"]
MANUFACTURER = "Somfy"
DEFAULT_PORT = 44100

View File

@ -2,49 +2,66 @@
import logging
from homeassistant.components.cover import (
DEVICE_CLASS_BLIND,
DEVICE_CLASS_SHUTTER,
DEVICE_CLASS_WINDOW,
ENTITY_ID_FORMAT,
CoverEntity,
)
from homeassistant.const import STATE_CLOSED, STATE_OPEN
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import slugify
from . import CONF_DEFAULT_REVERSE, DATA_SOMFY_MYLINK
from .const import (
CONF_DEFAULT_REVERSE,
CONF_REVERSE,
DATA_SOMFY_MYLINK,
DOMAIN,
MANUFACTURER,
MYLINK_ENTITY_IDS,
MYLINK_STATUS,
)
_LOGGER = logging.getLogger(__name__)
MYLINK_COVER_TYPE_TO_DEVICE_CLASS = {0: DEVICE_CLASS_BLIND, 1: DEVICE_CLASS_SHUTTER}
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Discover and configure Somfy covers."""
if discovery_info is None:
return
somfy_mylink = hass.data[DATA_SOMFY_MYLINK]
data = hass.data[DOMAIN][config_entry.entry_id]
mylink_status = data[MYLINK_STATUS]
somfy_mylink = data[DATA_SOMFY_MYLINK]
mylink_entity_ids = data[MYLINK_ENTITY_IDS]
cover_list = []
try:
mylink_status = await somfy_mylink.status_info()
except TimeoutError:
_LOGGER.error(
"Unable to connect to the Somfy MyLink device, "
"please check your settings"
)
return
for cover in mylink_status["result"]:
entity_id = ENTITY_ID_FORMAT.format(slugify(cover["name"]))
entity_config = discovery_info.get(entity_id, {})
default_reverse = discovery_info[CONF_DEFAULT_REVERSE]
mylink_entity_ids.append(entity_id)
entity_config = config_entry.options.get(entity_id, {})
default_reverse = config_entry.options.get(CONF_DEFAULT_REVERSE)
cover_config = {}
cover_config["target_id"] = cover["targetID"]
cover_config["name"] = cover["name"]
cover_config["reverse"] = entity_config.get("reverse", default_reverse)
cover_config["device_class"] = MYLINK_COVER_TYPE_TO_DEVICE_CLASS.get(
cover.get("type"), DEVICE_CLASS_WINDOW
)
cover_config["reverse"] = entity_config.get(CONF_REVERSE, default_reverse)
cover_list.append(SomfyShade(somfy_mylink, **cover_config))
_LOGGER.info(
"Adding Somfy Cover: %s with targetID %s",
cover_config["name"],
cover_config["target_id"],
)
async_add_entities(cover_list)
class SomfyShade(CoverEntity):
class SomfyShade(RestoreEntity, CoverEntity):
"""Object for controlling a Somfy cover."""
def __init__(
@ -60,8 +77,16 @@ class SomfyShade(CoverEntity):
self._target_id = target_id
self._name = name
self._reverse = reverse
self._closed = None
self._is_opening = None
self._is_closing = None
self._device_class = device_class
@property
def should_poll(self):
"""No polling since assumed state."""
return False
@property
def unique_id(self):
"""Return the unique ID of this cover."""
@ -72,11 +97,6 @@ class SomfyShade(CoverEntity):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
return None
@property
def assumed_state(self):
"""Let HA know the integration is assumed state."""
@ -87,20 +107,72 @@ class SomfyShade(CoverEntity):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._device_class
async def async_open_cover(self, **kwargs):
"""Wrap Homeassistant calls to open the cover."""
if not self._reverse:
await self.somfy_mylink.move_up(self._target_id)
else:
await self.somfy_mylink.move_down(self._target_id)
@property
def is_opening(self):
"""Return if the cover is opening."""
return self._is_opening
@property
def is_closing(self):
"""Return if the cover is closing."""
return self._is_closing
@property
def is_closed(self) -> bool:
"""Return if the cover is closed."""
return self._closed
@property
def device_info(self):
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, self._target_id)},
"name": self._name,
"manufacturer": MANUFACTURER,
}
async def async_close_cover(self, **kwargs):
"""Wrap Homeassistant calls to close the cover."""
if not self._reverse:
await self.somfy_mylink.move_down(self._target_id)
else:
await self.somfy_mylink.move_up(self._target_id)
"""Close the cover."""
self._is_closing = True
self.async_write_ha_state()
try:
# Blocks until the close command is sent
if not self._reverse:
await self.somfy_mylink.move_down(self._target_id)
else:
await self.somfy_mylink.move_up(self._target_id)
self._closed = True
finally:
self._is_closing = None
self.async_write_ha_state()
async def async_open_cover(self, **kwargs):
"""Open the cover."""
self._is_opening = True
self.async_write_ha_state()
try:
# Blocks until the open command is sent
if not self._reverse:
await self.somfy_mylink.move_up(self._target_id)
else:
await self.somfy_mylink.move_down(self._target_id)
self._closed = False
finally:
self._is_opening = None
self.async_write_ha_state()
async def async_stop_cover(self, **kwargs):
"""Stop the cover."""
await self.somfy_mylink.move_stop(self._target_id)
async def async_added_to_hass(self):
"""Complete the initialization."""
await super().async_added_to_hass()
# Restore the last state
last_state = await self.async_get_last_state()
if last_state is not None and last_state.state in (
STATE_OPEN,
STATE_CLOSED,
):
self._closed = last_state.state == STATE_CLOSED

View File

@ -2,6 +2,9 @@
"domain": "somfy_mylink",
"name": "Somfy MyLink",
"documentation": "https://www.home-assistant.io/integrations/somfy_mylink",
"requirements": ["somfy-mylink-synergy==1.0.6"],
"codeowners": []
}
"requirements": [
"somfy-mylink-synergy==1.0.6"
],
"codeowners": ["@bdraco"],
"config_flow": true
}

View File

@ -0,0 +1,44 @@
{
"title": "Somfy MyLink",
"config": {
"step": {
"user": {
"description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"system_id": "System ID"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"init": {
"title": "Configure MyLink Entities",
"data": {
"default_reverse": "Default reversal status for unconfigured covers",
"entity_id": "Configure a specific entity."
}
},
"entity_config": {
"title": "Configure Entity",
"description": "Configure options for `{entity_id}`",
"data": {
"reverse": "Cover is reversed"
}
}
}
}
}

View File

@ -0,0 +1,44 @@
{
"title": "Somfy MyLink",
"config": {
"step": {
"user": {
"description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"system_id": "System ID"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"init": {
"title": "Configure MyLink Entities",
"data": {
"default_reverse": "Default reversal status for unconfigured covers",
"entity_id": "Configure a specific entity."
}
},
"entity_config": {
"title": "Configure Entity",
"description": "Configure options for `{entity_id}`",
"data": {
"reverse": "Cover is reversed"
}
}
}
}
}

View File

@ -190,6 +190,7 @@ FLOWS = [
"solarlog",
"soma",
"somfy",
"somfy_mylink",
"sonarr",
"songpal",
"sonos",

View File

@ -1014,6 +1014,9 @@ solaredge==0.0.2
# homeassistant.components.honeywell
somecomfort==0.5.2
# homeassistant.components.somfy_mylink
somfy-mylink-synergy==1.0.6
# homeassistant.components.sonarr
sonarr==0.3.0

View File

@ -0,0 +1 @@
"""Tests for the Somfy MyLink integration."""

View File

@ -0,0 +1,385 @@
"""Test the Somfy MyLink config flow."""
import asyncio
from unittest.mock import patch
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.somfy_mylink.const import (
CONF_DEFAULT_REVERSE,
CONF_ENTITY_CONFIG,
CONF_REVERSE,
CONF_SYSTEM_ID,
DOMAIN,
)
from homeassistant.const import CONF_HOST, CONF_PORT
from tests.common import MockConfigEntry
async def test_form_user(hass):
"""Test we get the form."""
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"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info",
return_value={"any": "data"},
), patch(
"homeassistant.components.somfy_mylink.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.somfy_mylink.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "MyLink 1.1.1.1"
assert result2["data"] == {
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_already_configured(hass):
"""Test we abort if already configured."""
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46},
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info",
return_value={"any": "data"},
), patch(
"homeassistant.components.somfy_mylink.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.somfy_mylink.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
},
)
await hass.async_block_till_done()
assert result2["type"] == "abort"
assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
async def test_form_import(hass):
"""Test we get the form with import source."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info",
return_value={"any": "data"},
), patch(
"homeassistant.components.somfy_mylink.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.somfy_mylink.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={
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
},
)
await hass.async_block_till_done()
assert result["type"] == "create_entry"
assert result["title"] == "MyLink 1.1.1.1"
assert result["data"] == {
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_import_with_entity_config(hass):
"""Test we can import entity config."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info",
return_value={"any": "data"},
), patch(
"homeassistant.components.somfy_mylink.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.somfy_mylink.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={
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
CONF_DEFAULT_REVERSE: True,
CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}},
},
)
await hass.async_block_till_done()
assert result["type"] == "create_entry"
assert result["title"] == "MyLink 1.1.1.1"
assert result["data"] == {
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
CONF_DEFAULT_REVERSE: True,
CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}},
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_import_already_exists(hass):
"""Test we get the form with import source."""
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46},
)
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info",
return_value={"any": "data"},
), patch(
"homeassistant.components.somfy_mylink.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.somfy_mylink.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={
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
},
)
await hass.async_block_till_done()
assert result["type"] == "abort"
assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
async def test_form_invalid_auth(hass):
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info",
return_value={
"jsonrpc": "2.0",
"error": {"code": -32652, "message": "Invalid auth"},
"id": 818,
},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info",
side_effect=asyncio.TimeoutError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_error(hass):
"""Test we handle broad exception."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info",
side_effect=ValueError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 1234,
CONF_SYSTEM_ID: 456,
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}
async def test_options_not_loaded(hass):
"""Test options will not display until loaded."""
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46},
)
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info",
return_value={"result": []},
):
result = await hass.config_entries.options.async_init(config_entry.entry_id)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
async def test_options_no_entities(hass):
"""Test we can configure default reverse."""
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46},
)
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info",
return_value={"result": []},
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"default_reverse": True},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {
"default_reverse": True,
}
await hass.async_block_till_done()
async def test_options_with_entities(hass):
"""Test we can configure reverse for an entity."""
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46},
)
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info",
return_value={
"result": [
{
"targetID": "a",
"name": "Master Window",
"type": 0,
}
]
},
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"default_reverse": True, "entity_id": "cover.master_window"},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
result3 = await hass.config_entries.options.async_configure(
result2["flow_id"],
user_input={"reverse": False},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
result4 = await hass.config_entries.options.async_configure(
result3["flow_id"],
user_input={"default_reverse": True, "entity_id": None},
)
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {
"default_reverse": True,
"entity_config": {"cover.master_window": {"reverse": False}},
"entity_config_version": 1,
}
await hass.async_block_till_done()