Skip to content

Commit

Permalink
Add config flow to frontier_silicon (#64365)
Browse files Browse the repository at this point in the history
* Add config_flow to frontier_silicon

* Add missing translation file

* Delay unique_id validation until radio_id can be determined

* Fix tests

* Improve tests

* Use FlowResultType

* Bump afsapi to 0.2.6

* Fix requirements_test_all.txt

* Stash ssdp, reauth and unignore flows for now

* Re-introduce SSDP flow

* hassfest changes

* Address review comments

* Small style update

* Fix tests

* Update integrations.json

* fix order in manifest.json

* fix black errors

* Apply suggestions from code review

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Address review comments

* fix black errors

* Use async_setup_platform instead of async_setup

* Address review comments on tests

* parameterize tests

* Remove discovery component changes from this PR

* Address review comments

* Apply suggestions from code review

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Add extra asserts to tests

* Restructure _async_step_device_config_if_needed

* Add return statement

* Update homeassistant/components/frontier_silicon/media_player.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
  • Loading branch information
wlcrs and epenet authored Mar 10, 2023
1 parent fde205c commit b8bda93
Show file tree
Hide file tree
Showing 14 changed files with 636 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,8 @@ omit =
homeassistant/components/fritzbox_callmonitor/__init__.py
homeassistant/components/fritzbox_callmonitor/base.py
homeassistant/components/fritzbox_callmonitor/sensor.py
homeassistant/components/frontier_silicon/const.py
homeassistant/components/frontier_silicon/__init__.py
homeassistant/components/frontier_silicon/browse_media.py
homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py
homeassistant/components/garadget/cover.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/frontend/ @home-assistant/frontend
/tests/components/frontend/ @home-assistant/frontend
/homeassistant/components/frontier_silicon/ @wlcrs
/tests/components/frontier_silicon/ @wlcrs
/homeassistant/components/fully_kiosk/ @cgarwood
/tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
Expand Down
46 changes: 45 additions & 1 deletion homeassistant/components/frontier_silicon/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,45 @@
"""The frontier_silicon component."""
"""The Frontier Silicon integration."""
from __future__ import annotations

import logging

from afsapi import AFSAPI, ConnectionError as FSConnectionError

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady

from .const import CONF_PIN, CONF_WEBFSAPI_URL, DOMAIN

PLATFORMS = [Platform.MEDIA_PLAYER]

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Frontier Silicon from a config entry."""

webfsapi_url = entry.data[CONF_WEBFSAPI_URL]
pin = entry.data[CONF_PIN]

afsapi = AFSAPI(webfsapi_url, pin)

try:
await afsapi.get_power()
except FSConnectionError as exception:
raise PlatformNotReady from exception

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = afsapi

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
178 changes: 178 additions & 0 deletions homeassistant/components/frontier_silicon/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""Config flow for Frontier Silicon Media Player integration."""
from __future__ import annotations

import logging
from typing import Any

from afsapi import AFSAPI, ConnectionError as FSConnectionError, InvalidPinException
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.data_entry_flow import FlowResult

from .const import CONF_PIN, CONF_WEBFSAPI_URL, DEFAULT_PIN, DEFAULT_PORT, DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
)

STEP_DEVICE_CONFIG_DATA_SCHEMA = vol.Schema(
{
vol.Required(
CONF_PIN,
default=DEFAULT_PIN,
): str,
}
)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Frontier Silicon Media Player."""

VERSION = 1

def __init__(self) -> None:
"""Initialize flow."""

self._webfsapi_url: str | None = None
self._name: str | None = None
self._unique_id: str | None = None

async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult:
"""Handle the import of legacy configuration.yaml entries."""

device_url = f"http://{import_info[CONF_HOST]}:{import_info[CONF_PORT]}/device"
try:
self._webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url)
except FSConnectionError:
return self.async_abort(reason="cannot_connect")
except Exception as exception: # pylint: disable=broad-except
_LOGGER.exception(exception)
return self.async_abort(reason="unknown")

try:
afsapi = AFSAPI(self._webfsapi_url, import_info[CONF_PIN])

self._unique_id = await afsapi.get_radio_id()
except FSConnectionError:
return self.async_abort(reason="cannot_connect")
except InvalidPinException:
return self.async_abort(reason="invalid_auth")
except Exception as exception: # pylint: disable=broad-except
_LOGGER.exception(exception)
return self.async_abort(reason="unknown")

await self.async_set_unique_id(self._unique_id, raise_on_progress=False)
self._abort_if_unique_id_configured()

self._name = import_info[CONF_NAME] or "Radio"

return await self._create_entry(pin=import_info[CONF_PIN])

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step of manual configuration."""
errors = {}

if user_input:
device_url = (
f"http://{user_input[CONF_HOST]}:{user_input[CONF_PORT]}/device"
)
try:
self._webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url)
except FSConnectionError:
errors["base"] = "cannot_connect"
except Exception as exception: # pylint: disable=broad-except
_LOGGER.exception(exception)
errors["base"] = "unknown"
else:
return await self._async_step_device_config_if_needed()

data_schema = self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
)
return self.async_show_form(
step_id="user", data_schema=data_schema, errors=errors
)

async def _async_step_device_config_if_needed(self) -> FlowResult:
"""Most users will not have changed the default PIN on their radio.
We try to use this default PIN, and only if this fails ask for it via `async_step_device_config`
"""

try:
# try to login with default pin
afsapi = AFSAPI(self._webfsapi_url, DEFAULT_PIN)

self._name = await afsapi.get_friendly_name()
except InvalidPinException:
# Ask for a PIN
return await self.async_step_device_config()

self.context["title_placeholders"] = {"name": self._name}

self._unique_id = await afsapi.get_radio_id()
await self.async_set_unique_id(self._unique_id)
self._abort_if_unique_id_configured()

return await self._create_entry()

async def async_step_device_config(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle device configuration step.
We ask for the PIN in this step.
"""
assert self._webfsapi_url is not None

if user_input is None:
return self.async_show_form(
step_id="device_config", data_schema=STEP_DEVICE_CONFIG_DATA_SCHEMA
)

errors = {}

try:
afsapi = AFSAPI(self._webfsapi_url, user_input[CONF_PIN])

self._name = await afsapi.get_friendly_name()

except FSConnectionError:
errors["base"] = "cannot_connect"
except InvalidPinException:
errors["base"] = "invalid_auth"
except Exception as exception: # pylint: disable=broad-except
_LOGGER.exception(exception)
errors["base"] = "unknown"
else:
self._unique_id = await afsapi.get_radio_id()
await self.async_set_unique_id(self._unique_id)
self._abort_if_unique_id_configured()
return await self._create_entry(pin=user_input[CONF_PIN])

data_schema = self.add_suggested_values_to_schema(
STEP_DEVICE_CONFIG_DATA_SCHEMA, user_input
)
return self.async_show_form(
step_id="device_config",
data_schema=data_schema,
errors=errors,
)

async def _create_entry(self, pin: str | None = None) -> FlowResult:
"""Create the entry."""
assert self._name is not None
assert self._webfsapi_url is not None

data = {CONF_WEBFSAPI_URL: self._webfsapi_url, CONF_PIN: pin or DEFAULT_PIN}

return self.async_create_entry(title=self._name, data=data)
3 changes: 3 additions & 0 deletions homeassistant/components/frontier_silicon/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Constants for the Frontier Silicon Media Player integration."""
DOMAIN = "frontier_silicon"

CONF_WEBFSAPI_URL = "webfsapi_url"
CONF_PIN = "pin"

DEFAULT_PIN = "1234"
DEFAULT_PORT = 80

Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/frontier_silicon/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"domain": "frontier_silicon",
"name": "Frontier Silicon",
"codeowners": ["@wlcrs"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/frontier_silicon",
"iot_class": "local_polling",
"requirements": ["afsapi==0.2.7"]
Expand Down
57 changes: 40 additions & 17 deletions homeassistant/components/frontier_silicon/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@
MediaPlayerState,
MediaType,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from .browse_media import browse_node, browse_top_level
from .const import DEFAULT_PIN, DEFAULT_PORT, DOMAIN, MEDIA_CONTENT_ID_PRESET
from .const import CONF_PIN, DEFAULT_PIN, DEFAULT_PORT, DOMAIN, MEDIA_CONTENT_ID_PRESET

_LOGGER = logging.getLogger(__name__)

Expand All @@ -49,7 +51,11 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Frontier Silicon platform."""
"""Set up the Frontier Silicon platform.
YAML is deprecated, and imported automatically.
SSDP discovery is temporarily retained - to be refactor subsequently.
"""
if discovery_info is not None:
webfsapi_url = await AFSAPI.get_webfsapi_endpoint(
discovery_info["ssdp_description"]
Expand All @@ -61,24 +67,41 @@ async def async_setup_platform(
[AFSAPIDevice(name, afsapi)],
True,
)

return

host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
password = config.get(CONF_PASSWORD)
name = config.get(CONF_NAME)
ir.async_create_issue(
hass,
DOMAIN,
"remove_yaml",
breaks_in_ha_version="2023.6.0",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="removed_yaml",
)

try:
webfsapi_url = await AFSAPI.get_webfsapi_endpoint(
f"http://{host}:{port}/device"
)
except FSConnectionError:
_LOGGER.error(
"Could not add the FSAPI device at %s:%s -> %s", host, port, password
)
return
afsapi = AFSAPI(webfsapi_url, password)
async_add_entities([AFSAPIDevice(name, afsapi)], True)
await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_NAME: config.get(CONF_NAME),
CONF_HOST: config.get(CONF_HOST),
CONF_PORT: config.get(CONF_PORT, DEFAULT_PORT),
CONF_PIN: config.get(CONF_PASSWORD, DEFAULT_PIN),
},
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Frontier Silicon entity."""

afsapi: AFSAPI = hass.data[DOMAIN][config_entry.entry_id]

async_add_entities([AFSAPIDevice(config_entry.title, afsapi)], True)


class AFSAPIDevice(MediaPlayerEntity):
Expand Down
35 changes: 35 additions & 0 deletions homeassistant/components/frontier_silicon/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"config": {
"flow_title": "{name}",
"step": {
"user": {
"title": "Frontier Silicon Setup",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
}
},
"device_config": {
"title": "Device Configuration",
"description": "The pin can be found via 'MENU button > Main Menu > System setting > Network > NetRemote PIN setup'",
"data": {
"pin": "[%key:common::config_flow::data::pin%]"
}
}
},
"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%]"
}
},
"issues": {
"removed_yaml": {
"title": "The Frontier Silicon YAML configuration has been removed",
"description": "Configuring Frontier Silicon using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"fritzbox",
"fritzbox_callmonitor",
"fronius",
"frontier_silicon",
"fully_kiosk",
"garages_amsterdam",
"gdacs",
Expand Down
Loading

0 comments on commit b8bda93

Please sign in to comment.