Add prefix path support to pyLoad integration (#139139)
* Add prefix path configuration support * fix typo * formatting * uppercase * changes * redact hostpull/139641/head
parent
c9abe76023
commit
0a3562aca3
|
@ -2,14 +2,18 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from aiohttp import CookieJar
|
||||
from pyloadapi import PyLoadAPI
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
|
@ -19,17 +23,14 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
|||
|
||||
from .coordinator import PyLoadConfigEntry, PyLoadCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool:
|
||||
"""Set up pyLoad from a config entry."""
|
||||
|
||||
url = (
|
||||
f"{'https' if entry.data[CONF_SSL] else 'http'}://"
|
||||
f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}/"
|
||||
)
|
||||
|
||||
session = async_create_clientsession(
|
||||
hass,
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
|
@ -37,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo
|
|||
)
|
||||
pyloadapi = PyLoadAPI(
|
||||
session,
|
||||
api_url=url,
|
||||
api_url=URL(entry.data[CONF_URL]),
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
@ -55,3 +56,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo
|
|||
async def async_unload_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool:
|
||||
"""Migrate config entry."""
|
||||
_LOGGER.debug(
|
||||
"Migrating configuration from version %s.%s", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
if entry.version == 1 and entry.minor_version == 0:
|
||||
url = URL.build(
|
||||
scheme="https" if entry.data[CONF_SSL] else "http",
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
).human_repr()
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_URL: url}, minor_version=1, version=1
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to configuration version %s.%s successful",
|
||||
entry.version,
|
||||
entry.minor_version,
|
||||
)
|
||||
return True
|
||||
|
|
|
@ -9,19 +9,17 @@ from typing import Any
|
|||
from aiohttp import CookieJar
|
||||
from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
|
@ -29,15 +27,18 @@ from homeassistant.helpers.selector import (
|
|||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Required(CONF_SSL, default=False): cv.boolean,
|
||||
vol.Required(CONF_URL): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.URL,
|
||||
autocomplete="url",
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_VERIFY_SSL, default=True): bool,
|
||||
vol.Required(CONF_USERNAME): TextSelector(
|
||||
TextSelectorConfig(
|
||||
|
@ -80,14 +81,9 @@ async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> Non
|
|||
user_input[CONF_VERIFY_SSL],
|
||||
cookie_jar=CookieJar(unsafe=True),
|
||||
)
|
||||
|
||||
url = (
|
||||
f"{'https' if user_input[CONF_SSL] else 'http'}://"
|
||||
f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}/"
|
||||
)
|
||||
pyload = PyLoadAPI(
|
||||
session,
|
||||
api_url=url,
|
||||
api_url=URL(user_input[CONF_URL]),
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
@ -99,6 +95,7 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
"""Handle a config flow for pyLoad."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
@ -106,9 +103,8 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(
|
||||
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||
)
|
||||
url = URL(user_input[CONF_URL]).human_repr()
|
||||
self._async_abort_entries_match({CONF_URL: url})
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except (CannotConnect, ParserError):
|
||||
|
@ -120,7 +116,14 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
errors["base"] = "unknown"
|
||||
else:
|
||||
title = DEFAULT_NAME
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data={
|
||||
**user_input,
|
||||
CONF_URL: url,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
|
@ -144,9 +147,8 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
reauth_entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
new_input = reauth_entry.data | user_input
|
||||
try:
|
||||
await validate_input(self.hass, new_input)
|
||||
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
||||
except (CannotConnect, ParserError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
|
@ -155,7 +157,9 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(reauth_entry, data=new_input)
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry, data_updates=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
|
@ -191,15 +195,18 @@ class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfig_entry,
|
||||
data=user_input,
|
||||
data={
|
||||
**user_input,
|
||||
CONF_URL: URL(user_input[CONF_URL]).human_repr(),
|
||||
},
|
||||
reload_even_if_entry_is_unchanged=False,
|
||||
)
|
||||
|
||||
suggested_values = user_input if user_input else reconfig_entry.data
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA,
|
||||
user_input or reconfig_entry.data,
|
||||
suggested_values,
|
||||
),
|
||||
description_placeholders={CONF_NAME: reconfig_entry.data[CONF_USERNAME]},
|
||||
errors=errors,
|
||||
|
|
|
@ -5,13 +5,15 @@ from __future__ import annotations
|
|||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.diagnostics import REDACTED, async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import PyLoadConfigEntry, PyLoadData
|
||||
|
||||
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_HOST}
|
||||
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_URL}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
|
@ -21,6 +23,9 @@ async def async_get_config_entry_diagnostics(
|
|||
pyload_data: PyLoadData = config_entry.runtime_data.data
|
||||
|
||||
return {
|
||||
"config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT),
|
||||
"config_entry_data": {
|
||||
**async_redact_data(dict(config_entry.data), TO_REDACT),
|
||||
CONF_URL: URL(config_entry.data[CONF_URL]).with_host(REDACTED).human_repr(),
|
||||
},
|
||||
"pyload_data": asdict(pyload_data),
|
||||
}
|
||||
|
|
|
@ -3,38 +3,30 @@
|
|||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the device running your pyLoad instance.",
|
||||
"url": "Specify the full URL of your pyLoad web interface, including the protocol (HTTP or HTTPS), hostname or IP address, port (pyLoad uses 8000 by default), and any path prefix if applicable.\nExample: `https://example.com:8000/path`",
|
||||
"username": "The username used to access the pyLoad instance.",
|
||||
"password": "The password associated with the pyLoad account.",
|
||||
"port": "pyLoad uses port 8000 by default.",
|
||||
"ssl": "If enabled, the connection to the pyLoad instance will use HTTPS.",
|
||||
"verify_ssl": "If checked, the SSL certificate will be validated to ensure a secure connection."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::pyload::config::step::user::data_description::host%]",
|
||||
"verify_ssl": "[%key:component::pyload::config::step::user::data_description::verify_ssl%]",
|
||||
"url": "[%key:component::pyload::config::step::user::data_description::url%]",
|
||||
"username": "[%key:component::pyload::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::pyload::config::step::user::data_description::password%]",
|
||||
"port": "[%key:component::pyload::config::step::user::data_description::port%]",
|
||||
"ssl": "[%key:component::pyload::config::step::user::data_description::ssl%]"
|
||||
"verify_ssl": "[%key:component::pyload::config::step::user::data_description::verify_ssl%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
|
|
|
@ -12,6 +12,7 @@ from homeassistant.const import (
|
|||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
|
@ -19,10 +20,8 @@ from homeassistant.const import (
|
|||
from tests.common import MockConfigEntry
|
||||
|
||||
USER_INPUT = {
|
||||
CONF_HOST: "pyload.local",
|
||||
CONF_URL: "https://pyload.local:8000/prefix",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_PORT: 8000,
|
||||
CONF_SSL: True,
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
@ -33,10 +32,8 @@ REAUTH_INPUT = {
|
|||
}
|
||||
|
||||
NEW_INPUT = {
|
||||
CONF_HOST: "pyload.local",
|
||||
CONF_URL: "https://pyload.local:8000/prefix",
|
||||
CONF_PASSWORD: "new-password",
|
||||
CONF_PORT: 8000,
|
||||
CONF_SSL: True,
|
||||
CONF_USERNAME: "new-username",
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
@ -97,5 +94,28 @@ def mock_pyloadapi() -> Generator[MagicMock]:
|
|||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock pyLoad configuration entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN, title=DEFAULT_NAME, data=USER_INPUT, entry_id="XXXXXXXXXXXXXX"
|
||||
domain=DOMAIN,
|
||||
title=DEFAULT_NAME,
|
||||
data=USER_INPUT,
|
||||
entry_id="XXXXXXXXXXXXXX",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry_migrate")
|
||||
def mock_config_entry_migrate() -> MockConfigEntry:
|
||||
"""Mock pyLoad configuration entry for migration."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=DEFAULT_NAME,
|
||||
data={
|
||||
CONF_HOST: "pyload.local",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_PORT: 8000,
|
||||
CONF_SSL: True,
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
version=1,
|
||||
minor_version=0,
|
||||
entry_id="XXXXXXXXXXXXXX",
|
||||
)
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
# name: test_diagnostics
|
||||
dict({
|
||||
'config_entry_data': dict({
|
||||
'host': '**REDACTED**',
|
||||
'password': '**REDACTED**',
|
||||
'port': 8000,
|
||||
'ssl': True,
|
||||
'url': 'https://**redacted**:8000/prefix',
|
||||
'username': '**REDACTED**',
|
||||
'verify_ssl': False,
|
||||
}),
|
||||
|
|
|
@ -8,6 +8,7 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
|||
import pytest
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import CONF_PATH, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
@ -88,3 +89,22 @@ async def test_coordinator_update_invalid_auth(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pyloadapi")
|
||||
async def test_migration(
|
||||
hass: HomeAssistant,
|
||||
config_entry_migrate: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config entry migration."""
|
||||
|
||||
config_entry_migrate.add_to_hass(hass)
|
||||
assert config_entry_migrate.data.get(CONF_PATH) is None
|
||||
|
||||
await hass.config_entries.async_setup(config_entry_migrate.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry_migrate.state is ConfigEntryState.LOADED
|
||||
assert config_entry_migrate.version == 1
|
||||
assert config_entry_migrate.minor_version == 1
|
||||
assert config_entry_migrate.data[CONF_URL] == "https://pyload.local:8000/"
|
||||
|
|
Loading…
Reference in New Issue