Add vlc telnet config flow (#57513)

This commit is contained in:
Martin Hjelmare 2021-10-15 20:46:58 +02:00 committed by GitHub
parent aeb00823aa
commit 31ccaac865
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 715 additions and 115 deletions

View file

@ -1173,6 +1173,7 @@ omit =
homeassistant/components/vilfo/const.py
homeassistant/components/vivotek/camera.py
homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/__init__.py
homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/volkszaehler/sensor.py
homeassistant/components/volumio/__init__.py

View file

@ -571,7 +571,7 @@ homeassistant/components/vicare/* @oischinger
homeassistant/components/vilfo/* @ManneW
homeassistant/components/vivotek/* @HarlemSquirrel
homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf @dmcc
homeassistant/components/vlc_telnet/* @rodripf @dmcc @MartinHjelmare
homeassistant/components/volkszaehler/* @fabaff
homeassistant/components/volumio/* @OnFreund
homeassistant/components/wake_on_lan/* @ntilley905

View file

@ -1 +1,67 @@
"""The vlc component."""
"""The VLC media player Telnet integration."""
from aiovlc.client import Client
from aiovlc.exceptions import AuthError, ConnectError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DATA_AVAILABLE, DATA_VLC, DOMAIN, LOGGER
PLATFORMS = ["media_player"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up VLC media player Telnet from a config entry."""
config = entry.data
host = config[CONF_HOST]
port = config[CONF_PORT]
password = config[CONF_PASSWORD]
vlc = Client(password=password, host=host, port=port)
available = True
try:
await vlc.connect()
except ConnectError as err:
LOGGER.warning("Failed to connect to VLC: %s. Trying again", err)
available = False
if available:
try:
await vlc.login()
except AuthError as err:
await disconnect_vlc(vlc)
raise ConfigEntryAuthFailed() from err
domain_data = hass.data.setdefault(DOMAIN, {})
domain_data[entry.entry_id] = {DATA_VLC: vlc, DATA_AVAILABLE: available}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
entry_data = hass.data[DOMAIN].pop(entry.entry_id)
vlc = entry_data[DATA_VLC]
await hass.async_add_executor_job(disconnect_vlc, vlc)
return unload_ok
async def disconnect_vlc(vlc: Client) -> None:
"""Disconnect from VLC."""
LOGGER.debug("Disconnecting from VLC")
try:
await vlc.disconnect()
except ConnectError as err:
LOGGER.warning("Connection error: %s", err)

View file

@ -0,0 +1,159 @@
"""Config flow for VLC media player Telnet integration."""
from __future__ import annotations
import logging
from typing import Any
from aiovlc.client import Client
from aiovlc.exceptions import AuthError, ConnectError
import voluptuous as vol
from homeassistant import core, exceptions
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
from homeassistant.data_entry_flow import FlowResult
from .const import DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema:
"""Return user form schema."""
user_input = user_input or {}
return vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
vol.Optional(
CONF_HOST, default=user_input.get(CONF_HOST, "localhost")
): str,
vol.Optional(
CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT)
): int,
}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
async def vlc_connect(vlc: Client) -> None:
"""Connect to VLC."""
await vlc.connect()
await vlc.login()
await vlc.disconnect()
async def validate_input(
hass: core.HomeAssistant, data: dict[str, Any]
) -> dict[str, str]:
"""Validate the user input allows us to connect."""
vlc = Client(
password=data[CONF_PASSWORD],
host=data[CONF_HOST],
port=data[CONF_PORT],
)
try:
await vlc_connect(vlc)
except ConnectError as err:
raise CannotConnect from err
except AuthError as err:
raise InvalidAuth from err
# CONF_NAME is only present in the imported YAML data.
return {"title": data.get(CONF_NAME) or data[CONF_HOST]}
class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for VLC media player Telnet."""
VERSION = 1
entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=user_form_schema(user_input)
)
self._async_abort_entries_match(
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
)
errors = {}
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=user_form_schema(user_input), errors=errors
)
async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult:
"""Handle the import step."""
return await self.async_step_user(user_input)
async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult:
"""Handle reauth flow."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert self.entry
self.context["title_placeholders"] = {"host": self.entry.data[CONF_HOST]}
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle reauth confirm."""
assert self.entry
errors = {}
if user_input is not None:
try:
await validate_input(self.hass, {**self.entry.data, **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:
self.hass.config_entries.async_update_entry(
self.entry,
data={
**self.entry.data,
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(self.entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={CONF_HOST: self.entry.data[CONF_HOST]},
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=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,9 @@
"""Integration shared constants."""
import logging
DATA_VLC = "vlc"
DATA_AVAILABLE = "available"
DEFAULT_NAME = "VLC-TELNET"
DEFAULT_PORT = 4212
DOMAIN = "vlc_telnet"
LOGGER = logging.getLogger(__package__)

View file

@ -1,8 +1,9 @@
{
"domain": "vlc_telnet",
"name": "VLC media player Telnet",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/vlc_telnet",
"requirements": ["python-telnet-vlc==2.0.1"],
"codeowners": ["@rodripf", "@dmcc"],
"requirements": ["aiovlc==0.1.0"],
"codeowners": ["@rodripf", "@dmcc", "@MartinHjelmare"],
"iot_class": "local_polling"
}

View file

@ -1,13 +1,11 @@
"""Provide functionality to interact with the vlc telnet interface."""
import logging
from __future__ import annotations
from python_telnet_vlc import (
CommandError,
ConnectionError as ConnErr,
LuaError,
ParseError,
VLCTelnet,
)
from datetime import datetime
from typing import Any
from aiovlc.client import Client
from aiovlc.exceptions import AuthError, CommandError, ConnectError
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
@ -25,6 +23,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
@ -33,17 +32,15 @@ from homeassistant.const import (
STATE_IDLE,
STATE_PAUSED,
STATE_PLAYING,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
from .const import DATA_AVAILABLE, DATA_VLC, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, LOGGER
DOMAIN = "vlc_telnet"
DEFAULT_NAME = "VLC-TELNET"
DEFAULT_PORT = 4212
MAX_VOLUME = 500
SUPPORT_VLC = (
@ -69,106 +66,129 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the vlc platform."""
add_entities(
[
VlcDevice(
config.get(CONF_NAME),
config.get(CONF_HOST),
config.get(CONF_PORT),
config.get(CONF_PASSWORD),
LOGGER.warning(
"Loading VLC media player Telnet integration via platform setup is deprecated; "
"Please remove it from your configuration"
)
],
True,
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the vlc platform."""
# CONF_NAME is only present in imported YAML.
name = entry.data.get(CONF_NAME) or DEFAULT_NAME
vlc = hass.data[DOMAIN][entry.entry_id][DATA_VLC]
available = hass.data[DOMAIN][entry.entry_id][DATA_AVAILABLE]
async_add_entities([VlcDevice(entry, vlc, name, available)], True)
class VlcDevice(MediaPlayerEntity):
"""Representation of a vlc player."""
def __init__(self, name, host, port, passwd):
def __init__(
self, config_entry: ConfigEntry, vlc: Client, name: str, available: bool
) -> None:
"""Initialize the vlc device."""
self._config_entry = config_entry
self._name = name
self._volume = None
self._muted = None
self._state = STATE_UNAVAILABLE
self._media_position_updated_at = None
self._media_position = None
self._media_duration = None
self._host = host
self._port = port
self._password = passwd
self._vlc = None
self._available = True
self._volume_bkp = 0
self._media_artist = ""
self._media_title = ""
self._volume: float | None = None
self._muted: bool | None = None
self._state: str | None = None
self._media_position_updated_at: datetime | None = None
self._media_position: int | None = None
self._media_duration: int | None = None
self._vlc = vlc
self._available = available
self._volume_bkp = 0.0
self._media_artist: str | None = None
self._media_title: str | None = None
config_entry_id = config_entry.entry_id
self._attr_unique_id = config_entry_id
self._attr_device_info = {
"name": name,
"identifiers": {(DOMAIN, config_entry_id)},
"manufacturer": "VideoLAN",
"entry_type": "service",
}
def update(self):
async def async_update(self) -> None:
"""Get the latest details from the device."""
if self._vlc is None:
if not self._available:
try:
self._vlc = VLCTelnet(self._host, self._password, self._port)
except (ConnErr, EOFError) as err:
if self._available:
_LOGGER.error("Connection error: %s", err)
self._available = False
self._vlc = None
await self._vlc.connect()
except ConnectError as err:
LOGGER.debug("Connection error: %s", err)
return
try:
await self._vlc.login()
except AuthError:
LOGGER.debug("Failed to login to VLC")
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._config_entry.entry_id)
)
return
self._state = STATE_IDLE
self._available = True
LOGGER.info("Connected to vlc host: %s", self._vlc.host)
try:
status = self._vlc.status()
_LOGGER.debug("Status: %s", status)
status = await self._vlc.status()
LOGGER.debug("Status: %s", status)
if status:
if "volume" in status:
self._volume = status["volume"] / MAX_VOLUME
else:
self._volume = None
if "state" in status:
state = status["state"]
self._volume = status.audio_volume / MAX_VOLUME
state = status.state
if state == "playing":
self._state = STATE_PLAYING
elif state == "paused":
self._state = STATE_PAUSED
else:
self._state = STATE_IDLE
else:
self._state = STATE_IDLE
if self._state != STATE_IDLE:
self._media_duration = self._vlc.get_length()
vlc_position = self._vlc.get_time()
self._media_duration = (await self._vlc.get_length()).length
time_output = await self._vlc.get_time()
vlc_position = time_output.time
# Check if current position is stale.
if vlc_position != self._media_position:
self._media_position_updated_at = dt_util.utcnow()
self._media_position = vlc_position
info = self._vlc.info()
_LOGGER.debug("Info: %s", info)
info = await self._vlc.info()
data = info.data
LOGGER.debug("Info data: %s", data)
if info:
self._media_artist = info.get(0, {}).get("artist")
self._media_title = info.get(0, {}).get("title")
self._media_artist = data.get(0, {}).get("artist")
self._media_title = data.get(0, {}).get("title")
if not self._media_title:
# Fall back to filename.
data_info = info.get("data")
data_info = data.get("data")
if data_info:
self._media_title = data_info["filename"]
except (CommandError, LuaError, ParseError) as err:
_LOGGER.error("Command error: %s", err)
except (ConnErr, EOFError) as err:
except CommandError as err:
LOGGER.error("Command error: %s", err)
except ConnectError as err:
if self._available:
_LOGGER.error("Connection error: %s", err)
LOGGER.error("Connection error: %s", err)
self._available = False
self._vlc = None
@property
def name(self):
@ -186,7 +206,7 @@ class VlcDevice(MediaPlayerEntity):
return self._available
@property
def volume_level(self):
def volume_level(self) -> float | None:
"""Volume level of the media player (0..1)."""
return self._volume
@ -230,72 +250,79 @@ class VlcDevice(MediaPlayerEntity):
"""Artist of current playing media, music track only."""
return self._media_artist
def media_seek(self, position):
async def async_media_seek(self, position: float) -> None:
"""Seek the media to a specific location."""
self._vlc.seek(int(position))
await self._vlc.seek(round(position))
def mute_volume(self, mute):
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
assert self._volume is not None
if mute:
self._volume_bkp = self._volume
self.set_volume_level(0)
await self.async_set_volume_level(0)
else:
self.set_volume_level(self._volume_bkp)
await self.async_set_volume_level(self._volume_bkp)
self._muted = mute
def set_volume_level(self, volume):
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
self._vlc.set_volume(volume * MAX_VOLUME)
await self._vlc.set_volume(round(volume * MAX_VOLUME))
self._volume = volume
if self._muted and self._volume > 0:
# This can happen if we were muted and then see a volume_up.
self._muted = False
def media_play(self):
async def async_media_play(self) -> None:
"""Send play command."""
self._vlc.play()
await self._vlc.play()
self._state = STATE_PLAYING
def media_pause(self):
async def async_media_pause(self) -> None:
"""Send pause command."""
current_state = self._vlc.status().get("state")
status = await self._vlc.status()
current_state = status.state
if current_state != "paused":
# Make sure we're not already paused since VLCTelnet.pause() toggles
# pause.
self._vlc.pause()
await self._vlc.pause()
self._state = STATE_PAUSED
def media_stop(self):
async def async_media_stop(self) -> None:
"""Send stop command."""
self._vlc.stop()
await self._vlc.stop()
self._state = STATE_IDLE
def play_media(self, media_type, media_id, **kwargs):
async def async_play_media(
self, media_type: str, media_id: str, **kwargs: Any
) -> None:
"""Play media from a URL or file."""
if media_type != MEDIA_TYPE_MUSIC:
_LOGGER.error(
LOGGER.error(
"Invalid media type %s. Only %s is supported",
media_type,
MEDIA_TYPE_MUSIC,
)
return
self._vlc.add(media_id)
await self._vlc.add(media_id)
self._state = STATE_PLAYING
def media_previous_track(self):
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
self._vlc.prev()
await self._vlc.prev()
def media_next_track(self):
async def async_media_next_track(self) -> None:
"""Send next track command."""
self._vlc.next()
await self._vlc.next()
def clear_playlist(self):
async def async_clear_playlist(self) -> None:
"""Clear players playlist."""
self._vlc.clear()
await self._vlc.clear()
def set_shuffle(self, shuffle):
async def async_set_shuffle(self, shuffle: bool) -> None:
"""Enable/disable shuffle mode."""
self._vlc.random(shuffle)
shuffle_command = "on" if shuffle else "off"
await self._vlc.random(shuffle_command)

View file

@ -0,0 +1,30 @@
{
"config": {
"flow_title": "{host}",
"step": {
"reauth_confirm": {
"description": "Please enter the correct password for host: {host}",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"password": "[%key:common::config_flow::data::password%]",
"name": "[%key:common::config_flow::data::name%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"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%]"
}
}
}

View file

@ -0,0 +1,30 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"flow_title": "{host}",
"step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"description": "Please enter the correct password for host: {host}"
},
"user": {
"data": {
"host": "Host",
"name": "Name",
"password": "Password",
"port": "Port"
}
}
}
}
}

View file

@ -309,6 +309,7 @@ FLOWS = [
"vesync",
"vilfo",
"vizio",
"vlc_telnet",
"volumio",
"wallbox",
"watttime",

View file

@ -254,6 +254,9 @@ aiotractive==0.5.2
# homeassistant.components.unifi
aiounifi==27
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
# homeassistant.components.watttime
aiowatttime==0.1.1
@ -1930,9 +1933,6 @@ python-tado==0.12.0
# homeassistant.components.telegram_bot
python-telegram-bot==13.1
# homeassistant.components.vlc_telnet
python-telnet-vlc==2.0.1
# homeassistant.components.twitch
python-twitch-client==0.6.0

View file

@ -181,6 +181,9 @@ aiotractive==0.5.2
# homeassistant.components.unifi
aiounifi==27
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
# homeassistant.components.watttime
aiowatttime==0.1.1

View file

@ -0,0 +1 @@
"""Test the VLC media player Telnet integration."""

View file

@ -0,0 +1,272 @@
"""Test the VLC media player Telnet config flow."""
from __future__ import annotations
from typing import Any
from unittest.mock import patch
from aiovlc.exceptions import AuthError, ConnectError
import pytest
from homeassistant import config_entries
from homeassistant.components.vlc_telnet.const import DOMAIN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
# mypy: allow-untyped-calls
@pytest.mark.parametrize(
"input_data, entry_data",
[
(
{
"password": "test-password",
"host": "1.1.1.1",
"port": 8888,
},
{
"password": "test-password",
"host": "1.1.1.1",
"port": 8888,
},
),
(
{
"password": "test-password",
},
{
"password": "test-password",
"host": "localhost",
"port": 4212,
},
),
],
)
async def test_user_flow(
hass: HomeAssistant, input_data: dict[str, Any], entry_data: dict[str, Any]
) -> None:
"""Test successful user flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] is None
with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.login"
), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.disconnect"
), patch(
"homeassistant.components.vlc_telnet.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
input_data,
)
await hass.async_block_till_done()
assert result["type"] == "create_entry"
assert result["title"] == entry_data["host"]
assert result["data"] == entry_data
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_flow(hass: HomeAssistant) -> None:
"""Test successful import flow."""
with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.login"
), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.disconnect"
), patch(
"homeassistant.components.vlc_telnet.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={
"password": "test-password",
"host": "1.1.1.1",
"port": 8888,
"name": "custom name",
},
)
await hass.async_block_till_done()
assert result["type"] == "create_entry"
assert result["title"] == "custom name"
assert result["data"] == {
"password": "test-password",
"host": "1.1.1.1",
"port": 8888,
"name": "custom name",
}
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
"source", [config_entries.SOURCE_USER, config_entries.SOURCE_IMPORT]
)
async def test_abort_already_configured(hass: HomeAssistant, source: str) -> None:
"""Test we handle already configured host."""
entry_data = {
"password": "test-password",
"host": "1.1.1.1",
"port": 8888,
"name": "custom name",
}
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": source},
data=entry_data,
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
@pytest.mark.parametrize(
"source", [config_entries.SOURCE_USER, config_entries.SOURCE_IMPORT]
)
@pytest.mark.parametrize(
"error, connect_side_effect, login_side_effect",
[
("invalid_auth", None, AuthError),
("cannot_connect", ConnectError, None),
("unknown", Exception, None),
],
)
async def test_errors(
hass: HomeAssistant,
error: str,
connect_side_effect: Exception | None,
login_side_effect: Exception | None,
source: str,
) -> None:
"""Test we handle form errors."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": source}
)
with patch(
"homeassistant.components.vlc_telnet.config_flow.Client.connect",
side_effect=connect_side_effect,
), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.login",
side_effect=login_side_effect,
), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.disconnect"
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"password": "test-password"},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": error}
async def test_reauth_flow(hass: HomeAssistant) -> None:
"""Test successful reauth flow."""
entry_data = {
"password": "old-password",
"host": "1.1.1.1",
"port": 8888,
"name": "custom name",
}
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
data=entry_data,
)
with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.login"
), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.disconnect"
), patch(
"homeassistant.components.vlc_telnet.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"password": "new-password"},
)
await hass.async_block_till_done()
assert result["type"] == "abort"
assert result["reason"] == "reauth_successful"
assert len(mock_setup_entry.mock_calls) == 1
assert dict(entry.data) == {
"password": "new-password",
"host": "1.1.1.1",
"port": 8888,
"name": "custom name",
}
@pytest.mark.parametrize(
"error, connect_side_effect, login_side_effect",
[
("invalid_auth", None, AuthError),
("cannot_connect", ConnectError, None),
("unknown", Exception, None),
],
)
async def test_reauth_errors(
hass: HomeAssistant,
error: str,
connect_side_effect: Exception | None,
login_side_effect: Exception | None,
) -> None:
"""Test we handle reauth errors."""
entry_data = {
"password": "old-password",
"host": "1.1.1.1",
"port": 8888,
"name": "custom name",
}
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
data=entry_data,
)
with patch(
"homeassistant.components.vlc_telnet.config_flow.Client.connect",
side_effect=connect_side_effect,
), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.login",
side_effect=login_side_effect,
), patch(
"homeassistant.components.vlc_telnet.config_flow.Client.disconnect"
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"password": "test-password"},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": error}