Add prefix path support to pyLoad integration (#139139)

* Add prefix path configuration support

* fix typo

* formatting

* uppercase

* changes

* redact host
pull/139641/head
Manu 2025-03-02 16:45:57 +01:00 committed by GitHub
parent c9abe76023
commit 0a3562aca3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 126 additions and 59 deletions

View File

@ -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

View File

@ -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,

View File

@ -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),
}

View File

@ -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": {

View File

@ -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",
)

View File

@ -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,
}),

View File

@ -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/"