Add config flow to enigma2 (#106348)
* add config flow to enigma2 * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * fix suggested change * use parametrize for config flow tests * Restore PLATFORM_SCHEMA and add create_issue to async_setup_platform * fix docstring * remove name, refactor config flow * bump dependency * remove name, add verify_ssl, use async_create_clientsession * use translation key, change integration type to device * Bump openwebifpy to 4.2.1 * cleanup, remove CONF_NAME from entity, add async_set_unique_id * clear unneeded constants, fix tests * fix tests * move _attr_translation_key out of init * update test requirement * Address review comments * address review comments * clear strings.json * Review coments --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
764a0f29cc
commit
be0926b7b8
14 changed files with 565 additions and 40 deletions
|
@ -389,6 +389,7 @@ build.json @home-assistant/supervisor
|
||||||
/homeassistant/components/energyzero/ @klaasnicolaas
|
/homeassistant/components/energyzero/ @klaasnicolaas
|
||||||
/tests/components/energyzero/ @klaasnicolaas
|
/tests/components/energyzero/ @klaasnicolaas
|
||||||
/homeassistant/components/enigma2/ @autinerd
|
/homeassistant/components/enigma2/ @autinerd
|
||||||
|
/tests/components/enigma2/ @autinerd
|
||||||
/homeassistant/components/enocean/ @bdurrer
|
/homeassistant/components/enocean/ @bdurrer
|
||||||
/tests/components/enocean/ @bdurrer
|
/tests/components/enocean/ @bdurrer
|
||||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
||||||
|
|
|
@ -1 +1,48 @@
|
||||||
"""Support for Enigma2 devices."""
|
"""Support for Enigma2 devices."""
|
||||||
|
|
||||||
|
from openwebif.api import OpenWebIfDevice
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Enigma2 from a config entry."""
|
||||||
|
base_url = URL.build(
|
||||||
|
scheme="http" if not entry.data[CONF_SSL] else "https",
|
||||||
|
host=entry.data[CONF_HOST],
|
||||||
|
port=entry.data[CONF_PORT],
|
||||||
|
user=entry.data.get(CONF_USERNAME),
|
||||||
|
password=entry.data.get(CONF_PASSWORD),
|
||||||
|
)
|
||||||
|
|
||||||
|
session = async_create_clientsession(
|
||||||
|
hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = OpenWebIfDevice(session)
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
158
homeassistant/components/enigma2/config_flow.py
Normal file
158
homeassistant/components/enigma2/config_flow.py
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
"""Config flow for Enigma2."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp.client_exceptions import ClientError
|
||||||
|
from openwebif.api import OpenWebIfDevice
|
||||||
|
from openwebif.error import InvalidAuthError
|
||||||
|
import voluptuous as vol
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
|
from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers import selector
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_DEEP_STANDBY,
|
||||||
|
CONF_SOURCE_BOUQUET,
|
||||||
|
CONF_USE_CHANNEL_ICON,
|
||||||
|
DEFAULT_PORT,
|
||||||
|
DEFAULT_SSL,
|
||||||
|
DEFAULT_VERIFY_SSL,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): selector.TextSelector(),
|
||||||
|
vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.All(
|
||||||
|
selector.NumberSelector(
|
||||||
|
selector.NumberSelectorConfig(
|
||||||
|
min=1, max=65535, mode=selector.NumberSelectorMode.BOX
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Coerce(int),
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_USERNAME): selector.TextSelector(),
|
||||||
|
vol.Optional(CONF_PASSWORD): selector.TextSelector(
|
||||||
|
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||||
|
),
|
||||||
|
vol.Required(CONF_SSL, default=DEFAULT_SSL): selector.BooleanSelector(),
|
||||||
|
vol.Required(
|
||||||
|
CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL
|
||||||
|
): selector.BooleanSelector(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Enigma2."""
|
||||||
|
|
||||||
|
DATA_KEYS = (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
OPTIONS_KEYS = (CONF_DEEP_STANDBY, CONF_SOURCE_BOUQUET, CONF_USE_CHANNEL_ICON)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
super().__init__()
|
||||||
|
self.errors: dict[str, str] = {}
|
||||||
|
self._data: dict[str, Any] = {}
|
||||||
|
self._options: dict[str, Any] = {}
|
||||||
|
|
||||||
|
async def validate_user_input(self, user_input: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Validate user input."""
|
||||||
|
|
||||||
|
self.errors = {}
|
||||||
|
|
||||||
|
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||||
|
|
||||||
|
base_url = URL.build(
|
||||||
|
scheme="http" if not user_input[CONF_SSL] else "https",
|
||||||
|
host=user_input[CONF_HOST],
|
||||||
|
port=user_input[CONF_PORT],
|
||||||
|
user=user_input.get(CONF_USERNAME),
|
||||||
|
password=user_input.get(CONF_PASSWORD),
|
||||||
|
)
|
||||||
|
|
||||||
|
session = async_create_clientsession(
|
||||||
|
self.hass, verify_ssl=user_input[CONF_VERIFY_SSL], base_url=base_url
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
about = await OpenWebIfDevice(session).get_about()
|
||||||
|
except InvalidAuthError:
|
||||||
|
self.errors["base"] = "invalid_auth"
|
||||||
|
except ClientError:
|
||||||
|
self.errors["base"] = "cannot_connect"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
self.errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(about["info"]["ifaces"][0]["mac"])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
return user_input
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the user step."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA)
|
||||||
|
|
||||||
|
data = await self.validate_user_input(user_input)
|
||||||
|
if "base" in self.errors:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA, errors=self.errors
|
||||||
|
)
|
||||||
|
return self.async_create_entry(
|
||||||
|
data=data, title=data[CONF_HOST], options=self._options
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||||
|
"""Validate import."""
|
||||||
|
if CONF_PORT not in user_input:
|
||||||
|
user_input[CONF_PORT] = DEFAULT_PORT
|
||||||
|
if CONF_SSL not in user_input:
|
||||||
|
user_input[CONF_SSL] = DEFAULT_SSL
|
||||||
|
user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL
|
||||||
|
|
||||||
|
async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
HOMEASSISTANT_DOMAIN,
|
||||||
|
f"deprecated_yaml_{DOMAIN}",
|
||||||
|
breaks_in_ha_version="2024.11.0",
|
||||||
|
is_fixable=False,
|
||||||
|
is_persistent=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml",
|
||||||
|
translation_placeholders={
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"integration_title": "Enigma2",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._data = {
|
||||||
|
key: user_input[key] for key in user_input if key in self.DATA_KEYS
|
||||||
|
}
|
||||||
|
self._options = {
|
||||||
|
key: user_input[key] for key in user_input if key in self.OPTIONS_KEYS
|
||||||
|
}
|
||||||
|
|
||||||
|
return await self.async_step_user(self._data)
|
|
@ -16,3 +16,4 @@ DEFAULT_PASSWORD = "dreambox"
|
||||||
DEFAULT_DEEP_STANDBY = False
|
DEFAULT_DEEP_STANDBY = False
|
||||||
DEFAULT_SOURCE_BOUQUET = ""
|
DEFAULT_SOURCE_BOUQUET = ""
|
||||||
DEFAULT_MAC_ADDRESS = ""
|
DEFAULT_MAC_ADDRESS = ""
|
||||||
|
DEFAULT_VERIFY_SSL = False
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
"domain": "enigma2",
|
"domain": "enigma2",
|
||||||
"name": "Enigma2 (OpenWebif)",
|
"name": "Enigma2 (OpenWebif)",
|
||||||
"codeowners": ["@autinerd"],
|
"codeowners": ["@autinerd"],
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/enigma2",
|
"documentation": "https://www.home-assistant.io/integrations/enigma2",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["openwebif"],
|
"loggers": ["openwebif"],
|
||||||
"requirements": ["openwebifpy==4.2.4"]
|
"requirements": ["openwebifpy==4.2.4"]
|
||||||
|
|
|
@ -9,7 +9,6 @@ from aiohttp.client_exceptions import ClientConnectorError, ServerDisconnectedEr
|
||||||
from openwebif.api import OpenWebIfDevice
|
from openwebif.api import OpenWebIfDevice
|
||||||
from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption
|
from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from yarl import URL
|
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MediaPlayerEntity,
|
MediaPlayerEntity,
|
||||||
|
@ -17,6 +16,7 @@ from homeassistant.components.media_player import (
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
MediaType,
|
MediaType,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
@ -26,10 +26,9 @@ from homeassistant.const import (
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
@ -47,6 +46,7 @@ from .const import (
|
||||||
DEFAULT_SSL,
|
DEFAULT_SSL,
|
||||||
DEFAULT_USE_CHANNEL_ICON,
|
DEFAULT_USE_CHANNEL_ICON,
|
||||||
DEFAULT_USERNAME,
|
DEFAULT_USERNAME,
|
||||||
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording"
|
ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording"
|
||||||
|
@ -81,49 +81,44 @@ async def async_setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up of an enigma2 media player."""
|
"""Set up of an enigma2 media player."""
|
||||||
if discovery_info:
|
|
||||||
# Discovery gives us the streaming service port (8001)
|
|
||||||
# which is not useful as OpenWebif never runs on that port.
|
|
||||||
# So use the default port instead.
|
|
||||||
config[CONF_PORT] = DEFAULT_PORT
|
|
||||||
config[CONF_NAME] = discovery_info["hostname"]
|
|
||||||
config[CONF_HOST] = discovery_info["host"]
|
|
||||||
config[CONF_USERNAME] = DEFAULT_USERNAME
|
|
||||||
config[CONF_PASSWORD] = DEFAULT_PASSWORD
|
|
||||||
config[CONF_SSL] = DEFAULT_SSL
|
|
||||||
config[CONF_USE_CHANNEL_ICON] = DEFAULT_USE_CHANNEL_ICON
|
|
||||||
config[CONF_MAC_ADDRESS] = DEFAULT_MAC_ADDRESS
|
|
||||||
config[CONF_DEEP_STANDBY] = DEFAULT_DEEP_STANDBY
|
|
||||||
config[CONF_SOURCE_BOUQUET] = DEFAULT_SOURCE_BOUQUET
|
|
||||||
|
|
||||||
base_url = URL.build(
|
entry_data = {
|
||||||
scheme="https" if config[CONF_SSL] else "http",
|
CONF_HOST: config[CONF_HOST],
|
||||||
host=config[CONF_HOST],
|
CONF_PORT: config[CONF_PORT],
|
||||||
port=config.get(CONF_PORT),
|
CONF_USERNAME: config[CONF_USERNAME],
|
||||||
user=config.get(CONF_USERNAME),
|
CONF_PASSWORD: config[CONF_PASSWORD],
|
||||||
password=config.get(CONF_PASSWORD),
|
CONF_SSL: config[CONF_SSL],
|
||||||
|
CONF_USE_CHANNEL_ICON: config[CONF_USE_CHANNEL_ICON],
|
||||||
|
CONF_DEEP_STANDBY: config[CONF_DEEP_STANDBY],
|
||||||
|
CONF_SOURCE_BOUQUET: config[CONF_SOURCE_BOUQUET],
|
||||||
|
}
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_data
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
session = async_create_clientsession(hass, verify_ssl=False, base_url=base_url)
|
|
||||||
|
|
||||||
device = OpenWebIfDevice(
|
async def async_setup_entry(
|
||||||
host=session,
|
hass: HomeAssistant,
|
||||||
turn_off_to_deep=config.get(CONF_DEEP_STANDBY, False),
|
entry: ConfigEntry,
|
||||||
source_bouquet=config.get(CONF_SOURCE_BOUQUET),
|
async_add_entities: AddEntitiesCallback,
|
||||||
)
|
) -> None:
|
||||||
|
"""Set up the Enigma2 media player platform."""
|
||||||
|
|
||||||
try:
|
device: OpenWebIfDevice = hass.data[DOMAIN][entry.entry_id]
|
||||||
about = await device.get_about()
|
about = await device.get_about()
|
||||||
except ClientConnectorError as err:
|
device.mac_address = about["info"]["ifaces"][0]["mac"]
|
||||||
raise PlatformNotReady from err
|
entity = Enigma2Device(entry, device, about)
|
||||||
|
async_add_entities([entity])
|
||||||
async_add_entities([Enigma2Device(config[CONF_NAME], device, about)])
|
|
||||||
|
|
||||||
|
|
||||||
class Enigma2Device(MediaPlayerEntity):
|
class Enigma2Device(MediaPlayerEntity):
|
||||||
"""Representation of an Enigma2 box."""
|
"""Representation of an Enigma2 box."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
_attr_media_content_type = MediaType.TVSHOW
|
_attr_media_content_type = MediaType.TVSHOW
|
||||||
_attr_supported_features = (
|
_attr_supported_features = (
|
||||||
|
@ -139,14 +134,23 @@ class Enigma2Device(MediaPlayerEntity):
|
||||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, name: str, device: OpenWebIfDevice, about: dict) -> None:
|
def __init__(
|
||||||
|
self, entry: ConfigEntry, device: OpenWebIfDevice, about: dict
|
||||||
|
) -> None:
|
||||||
"""Initialize the Enigma2 device."""
|
"""Initialize the Enigma2 device."""
|
||||||
self._device: OpenWebIfDevice = device
|
self._device: OpenWebIfDevice = device
|
||||||
self._device.mac_address = about["info"]["ifaces"][0]["mac"]
|
self._entry = entry
|
||||||
|
|
||||||
self._attr_name = name
|
|
||||||
self._attr_unique_id = device.mac_address
|
self._attr_unique_id = device.mac_address
|
||||||
|
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, device.mac_address)},
|
||||||
|
manufacturer=about["info"]["brand"],
|
||||||
|
model=about["info"]["model"],
|
||||||
|
configuration_url=device.base,
|
||||||
|
name=entry.data[CONF_HOST],
|
||||||
|
)
|
||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
async def async_turn_off(self) -> None:
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
if self._device.turn_off_to_deep:
|
if self._device.turn_off_to_deep:
|
||||||
|
|
30
homeassistant/components/enigma2/strings.json
Normal file
30
homeassistant/components/enigma2/strings.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "{name}",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"description": "Please enter the connection details of your device.",
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
|
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"name": "[%key:common::config_flow::data::name%]",
|
||||||
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,6 +148,7 @@ FLOWS = {
|
||||||
"emulated_roku",
|
"emulated_roku",
|
||||||
"energenie_power_sockets",
|
"energenie_power_sockets",
|
||||||
"energyzero",
|
"energyzero",
|
||||||
|
"enigma2",
|
||||||
"enocean",
|
"enocean",
|
||||||
"enphase_envoy",
|
"enphase_envoy",
|
||||||
"environment_canada",
|
"environment_canada",
|
||||||
|
|
|
@ -1604,8 +1604,8 @@
|
||||||
},
|
},
|
||||||
"enigma2": {
|
"enigma2": {
|
||||||
"name": "Enigma2 (OpenWebif)",
|
"name": "Enigma2 (OpenWebif)",
|
||||||
"integration_type": "hub",
|
"integration_type": "device",
|
||||||
"config_flow": false,
|
"config_flow": true,
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
},
|
},
|
||||||
"enmax": {
|
"enmax": {
|
||||||
|
|
|
@ -1173,6 +1173,9 @@ openerz-api==0.3.0
|
||||||
# homeassistant.components.openhome
|
# homeassistant.components.openhome
|
||||||
openhomedevice==2.2.0
|
openhomedevice==2.2.0
|
||||||
|
|
||||||
|
# homeassistant.components.enigma2
|
||||||
|
openwebifpy==4.2.4
|
||||||
|
|
||||||
# homeassistant.components.opower
|
# homeassistant.components.opower
|
||||||
opower==0.4.3
|
opower==0.4.3
|
||||||
|
|
||||||
|
|
1
tests/components/enigma2/__init__.py
Normal file
1
tests/components/enigma2/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the Enigma2 integration."""
|
90
tests/components/enigma2/conftest.py
Normal file
90
tests/components/enigma2/conftest.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
"""Test the Enigma2 config flow."""
|
||||||
|
|
||||||
|
from homeassistant.components.enigma2.const import (
|
||||||
|
CONF_DEEP_STANDBY,
|
||||||
|
CONF_MAC_ADDRESS,
|
||||||
|
CONF_SOURCE_BOUQUET,
|
||||||
|
CONF_USE_CHANNEL_ICON,
|
||||||
|
DEFAULT_DEEP_STANDBY,
|
||||||
|
DEFAULT_PORT,
|
||||||
|
DEFAULT_SSL,
|
||||||
|
DEFAULT_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
|
||||||
|
MAC_ADDRESS = "12:34:56:78:90:ab"
|
||||||
|
|
||||||
|
TEST_REQUIRED = {
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PORT: DEFAULT_PORT,
|
||||||
|
CONF_SSL: DEFAULT_SSL,
|
||||||
|
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_FULL = {
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PORT: DEFAULT_PORT,
|
||||||
|
CONF_SSL: DEFAULT_SSL,
|
||||||
|
CONF_USERNAME: "root",
|
||||||
|
CONF_PASSWORD: "password",
|
||||||
|
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_IMPORT_FULL = {
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PORT: DEFAULT_PORT,
|
||||||
|
CONF_SSL: DEFAULT_SSL,
|
||||||
|
CONF_USERNAME: "root",
|
||||||
|
CONF_PASSWORD: "password",
|
||||||
|
CONF_NAME: "My Player",
|
||||||
|
CONF_DEEP_STANDBY: DEFAULT_DEEP_STANDBY,
|
||||||
|
CONF_SOURCE_BOUQUET: "Favourites",
|
||||||
|
CONF_MAC_ADDRESS: MAC_ADDRESS,
|
||||||
|
CONF_USE_CHANNEL_ICON: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_IMPORT_REQUIRED = {CONF_HOST: "1.1.1.1"}
|
||||||
|
|
||||||
|
EXPECTED_OPTIONS = {
|
||||||
|
CONF_DEEP_STANDBY: DEFAULT_DEEP_STANDBY,
|
||||||
|
CONF_SOURCE_BOUQUET: "Favourites",
|
||||||
|
CONF_USE_CHANNEL_ICON: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MockDevice:
|
||||||
|
"""A mock Enigma2 device."""
|
||||||
|
|
||||||
|
mac_address: str | None = "12:34:56:78:90:ab"
|
||||||
|
_base = "http://1.1.1.1"
|
||||||
|
|
||||||
|
async def _call_api(self, url: str) -> dict:
|
||||||
|
if url.endswith("/api/about"):
|
||||||
|
return {
|
||||||
|
"info": {
|
||||||
|
"ifaces": [
|
||||||
|
{
|
||||||
|
"mac": self.mac_address,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
"""Return the version."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_about(self) -> dict:
|
||||||
|
"""Get mock about endpoint."""
|
||||||
|
return await self._call_api("/api/about")
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Mock close."""
|
149
tests/components/enigma2/test_config_flow.py
Normal file
149
tests/components/enigma2/test_config_flow.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
"""Test the Enigma2 config flow."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from aiohttp.client_exceptions import ClientError
|
||||||
|
from openwebif.error import InvalidAuthError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.enigma2.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_HOST
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from .conftest import (
|
||||||
|
EXPECTED_OPTIONS,
|
||||||
|
TEST_FULL,
|
||||||
|
TEST_IMPORT_FULL,
|
||||||
|
TEST_IMPORT_REQUIRED,
|
||||||
|
TEST_REQUIRED,
|
||||||
|
MockDevice,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def user_flow(hass: HomeAssistant) -> str:
|
||||||
|
"""Return a user-initiated flow after filling in host info."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] is None
|
||||||
|
return result["flow_id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_config"),
|
||||||
|
[(TEST_FULL), (TEST_REQUIRED)],
|
||||||
|
)
|
||||||
|
async def test_form_user(
|
||||||
|
hass: HomeAssistant, user_flow: str, test_config: dict[str, Any]
|
||||||
|
):
|
||||||
|
"""Test a successful user initiated flow."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"openwebif.api.OpenWebIfDevice.__new__",
|
||||||
|
return_value=MockDevice(),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.enigma2.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(user_flow, test_config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == test_config[CONF_HOST]
|
||||||
|
assert result["data"] == test_config
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "error_type"),
|
||||||
|
[
|
||||||
|
(InvalidAuthError, "invalid_auth"),
|
||||||
|
(ClientError, "cannot_connect"),
|
||||||
|
(Exception, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_form_user_errors(
|
||||||
|
hass: HomeAssistant, user_flow, exception: Exception, error_type: str
|
||||||
|
) -> None:
|
||||||
|
"""Test we handle errors."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__",
|
||||||
|
side_effect=exception,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(user_flow, TEST_FULL)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == config_entries.SOURCE_USER
|
||||||
|
assert result["errors"] == {"base": error_type}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_config", "expected_data", "expected_options"),
|
||||||
|
[
|
||||||
|
(TEST_IMPORT_FULL, TEST_FULL, EXPECTED_OPTIONS),
|
||||||
|
(TEST_IMPORT_REQUIRED, TEST_REQUIRED, {}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_form_import(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
test_config: dict[str, Any],
|
||||||
|
expected_data: dict[str, Any],
|
||||||
|
expected_options: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test we get the form with import source."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__",
|
||||||
|
return_value=MockDevice(),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.enigma2.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=test_config,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == test_config[CONF_HOST]
|
||||||
|
assert result["data"] == expected_data
|
||||||
|
assert result["options"] == expected_options
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "error_type"),
|
||||||
|
[
|
||||||
|
(InvalidAuthError, "invalid_auth"),
|
||||||
|
(ClientError, "cannot_connect"),
|
||||||
|
(Exception, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_form_import_errors(
|
||||||
|
hass: HomeAssistant, exception: Exception, error_type: str
|
||||||
|
) -> None:
|
||||||
|
"""Test we handle errors on import."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__",
|
||||||
|
side_effect=exception,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data=TEST_IMPORT_FULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": error_type}
|
38
tests/components/enigma2/test_init.py
Normal file
38
tests/components/enigma2/test_init.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
"""Test the Enigma2 integration init."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components.enigma2.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .conftest import TEST_REQUIRED, MockDevice
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unload_entry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test successful unload of entry."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.enigma2.OpenWebIfDevice.__new__",
|
||||||
|
return_value=MockDevice(),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.enigma2.media_player.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data=TEST_REQUIRED, title="name")
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||||
|
assert not hass.data.get(DOMAIN)
|
Loading…
Add table
Reference in a new issue