Refactor project into two standalone components
Split monorepo into separate units for future independent repositories: - media-server/: Standalone FastAPI server with own README, requirements, config example, and CLAUDE.md - haos-integration/: HACS-ready Home Assistant integration with hacs.json, own README, and CLAUDE.md Both components now have their own .gitignore files and can be easily extracted into separate repositories. Also adds custom icon support for scripts configuration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""Config flow for Remote Media Player integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .api_client import (
|
||||
MediaServerClient,
|
||||
MediaServerConnectionError,
|
||||
MediaServerAuthError,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_TOKEN,
|
||||
CONF_POLL_INTERVAL,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_POLL_INTERVAL,
|
||||
DEFAULT_NAME,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Args:
|
||||
hass: Home Assistant instance
|
||||
data: User input data
|
||||
|
||||
Returns:
|
||||
Validated data with title
|
||||
|
||||
Raises:
|
||||
CannotConnect: If connection fails
|
||||
InvalidAuth: If authentication fails
|
||||
"""
|
||||
client = MediaServerClient(
|
||||
host=data[CONF_HOST],
|
||||
port=data[CONF_PORT],
|
||||
token=data[CONF_TOKEN],
|
||||
)
|
||||
|
||||
try:
|
||||
health = await client.get_health()
|
||||
# Try authenticated endpoint
|
||||
await client.get_status()
|
||||
except MediaServerConnectionError as err:
|
||||
await client.close()
|
||||
raise CannotConnect(str(err)) from err
|
||||
except MediaServerAuthError as err:
|
||||
await client.close()
|
||||
raise InvalidAuth(str(err)) from err
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
# Return info to store in the config entry
|
||||
return {
|
||||
"title": data.get(CONF_NAME, DEFAULT_NAME),
|
||||
"platform": health.get("platform", "Unknown"),
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Remote Media Player."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step.
|
||||
|
||||
Args:
|
||||
user_input: User provided configuration
|
||||
|
||||
Returns:
|
||||
Flow result
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
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:
|
||||
# Check if already configured
|
||||
await self.async_set_unique_id(
|
||||
f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=info["title"],
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
# Show configuration form
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): selector.TextSelector(
|
||||
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
|
||||
),
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=1,
|
||||
max=65535,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_TOKEN): selector.TextSelector(
|
||||
selector.TextSelectorConfig(
|
||||
type=selector.TextSelectorType.PASSWORD
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): selector.TextSelector(
|
||||
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_POLL_INTERVAL, default=DEFAULT_POLL_INTERVAL
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=1,
|
||||
max=60,
|
||||
step=1,
|
||||
unit_of_measurement="seconds",
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> config_entries.OptionsFlow:
|
||||
"""Create the options flow.
|
||||
|
||||
Args:
|
||||
config_entry: Config entry
|
||||
|
||||
Returns:
|
||||
Options flow handler
|
||||
"""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle options flow for Remote Media Player."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow.
|
||||
|
||||
Args:
|
||||
config_entry: Config entry
|
||||
"""
|
||||
self._config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options.
|
||||
|
||||
Args:
|
||||
user_input: User provided options
|
||||
|
||||
Returns:
|
||||
Flow result
|
||||
"""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_POLL_INTERVAL,
|
||||
default=self._config_entry.options.get(
|
||||
CONF_POLL_INTERVAL,
|
||||
self._config_entry.data.get(
|
||||
CONF_POLL_INTERVAL, DEFAULT_POLL_INTERVAL
|
||||
),
|
||||
),
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=1,
|
||||
max=60,
|
||||
step=1,
|
||||
unit_of_measurement="seconds",
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(Exception):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(Exception):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
Reference in New Issue
Block a user