Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions homeassistant/components/prowl/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue

from .const import DOMAIN
from .helpers import async_verify_key
Expand Down Expand Up @@ -55,6 +56,30 @@ async def async_step_user(
errors=errors,
)

async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult:
"""Handle import from legacy YAML."""
api_key = config[CONF_API_KEY]
self._async_abort_entries_match({CONF_API_KEY: api_key})

errors = await self._validate_api_key(api_key)
if not errors:
return self.async_create_entry(
title=config[CONF_NAME],
data={
CONF_API_KEY: api_key,
},
)
async_create_issue(
self.hass,
DOMAIN,
"migrate_fail_prowl",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="prowl_yaml_migration_fail",
)
return self.async_abort(reason="invalid_api_key")

async def _validate_api_key(self, api_key: str) -> dict[str, str]:
"""Validate the provided API key."""
ret = {}
Expand Down
6 changes: 2 additions & 4 deletions homeassistant/components/prowl/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,5 @@ async def async_verify_key(hass: HomeAssistant, api_key: str) -> bool:
async with asyncio.timeout(10):
await prowl.verify_key()
return True
except prowlpy.APIError as ex:
if str(ex).startswith("Invalid API key"):
return False
raise
except prowlpy.InvalidAPIKeyError:
return False
56 changes: 39 additions & 17 deletions homeassistant/components/prowl/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@
PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA,
BaseNotificationService,
NotifyEntity,
migrate_notify_issue,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string})
Expand All @@ -38,6 +42,22 @@ async def async_get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> ProwlNotificationService:
"""Get the Prowl notification service."""
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)
async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2026.04.01",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="prowl_yaml_deprecated",
)

return ProwlNotificationService(hass, config[CONF_API_KEY], get_async_client(hass))


Expand Down Expand Up @@ -68,6 +88,8 @@ def __init__(

async def async_send_message(self, message: str, **kwargs: Any) -> None:
"""Send the message to the user."""
migrate_notify_issue(self._hass, DOMAIN, DOMAIN, "2026.04.01", DOMAIN)

data = kwargs.get(ATTR_DATA, {})
if data is None:
data = {}
Expand All @@ -84,15 +106,15 @@ async def async_send_message(self, message: str, **kwargs: Any) -> None:
except TimeoutError as ex:
_LOGGER.error("Timeout accessing Prowl API")
raise HomeAssistantError("Timeout accessing Prowl API") from ex
except prowlpy.InvalidAPIKeyError as ex:
_LOGGER.error("Invalid API key for Prowl service")
raise HomeAssistantError("Invalid API key for Prowl service") from ex
except prowlpy.RateLimitExceededError as ex:
_LOGGER.error("Prowl returned: exceeded rate limit")
raise HomeAssistantError(
"Prowl service reported: exceeded rate limit"
) from ex
except prowlpy.APIError as ex:
if str(ex).startswith("Invalid API key"):
_LOGGER.error("Invalid API key for Prowl service")
raise HomeAssistantError("Invalid API key for Prowl service") from ex
if str(ex).startswith("Not accepted"):
_LOGGER.error("Prowl returned: exceeded rate limit")
raise HomeAssistantError(
"Prowl service reported: exceeded rate limit"
) from ex
_LOGGER.error("Unexpected error when calling Prowl API: %s", str(ex))
raise HomeAssistantError("Unexpected error when calling Prowl API") from ex

Expand Down Expand Up @@ -131,14 +153,14 @@ async def async_send_message(self, message: str, title: str | None = None) -> No
except TimeoutError as ex:
_LOGGER.error("Timeout accessing Prowl API")
raise HomeAssistantError("Timeout accessing Prowl API") from ex
except prowlpy.InvalidAPIKeyError as ex:
_LOGGER.error("Invalid API key for Prowl service")
raise HomeAssistantError("Invalid API key for Prowl service") from ex
except prowlpy.RateLimitExceededError as ex:
_LOGGER.error("Prowl returned: exceeded rate limit")
raise HomeAssistantError(
"Prowl service reported: exceeded rate limit"
) from ex
except prowlpy.APIError as ex:
if str(ex).startswith("Invalid API key"):
_LOGGER.error("Invalid API key for Prowl service")
raise HomeAssistantError("Invalid API key for Prowl service") from ex
if str(ex).startswith("Not accepted"):
_LOGGER.error("Prowl returned: exceeded rate limit")
raise HomeAssistantError(
"Prowl service reported: exceeded rate limit"
) from ex
_LOGGER.error("Unexpected error when calling Prowl API: %s", str(ex))
raise HomeAssistantError("Unexpected error when calling Prowl API") from ex
10 changes: 10 additions & 0 deletions homeassistant/components/prowl/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,15 @@
"abort": {
"already_configured": "API key is already configured"
}
},
"issues": {
"prowl_yaml_deprecated": {
"title": "Prowl YAML configuration deprecated",
"description": "Configuring Prowl using YAML has been deprecated. An automatic import of your existing configuration has been made.\nPlease remove the Prowl configuration from configuration.yaml and update any automations to use the new `notify.send_message` action exposed with this new entity."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nd update any automations to use the new notify.send_message action exposed with this new entity.

Uh that sounds like a major breaking change or something that should be been raise repairs for separately; and not something that should be squeezed into this message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we do that too. https://github.com/home-assistant/core/pull/154727/files#diff-16a6406edebe12b764d2e6c7ccc098fdb11b4c7bcfcf1ab6edc46ce9f416f7ecR81 I just added it here to reinforce that there are more actions other than just migrating the YAML.
Should I perhaps phrase it better?

},
"prowl_yaml_migration_fail": {
"title": "Unable to migrate Prowl YAML configuration",
"description": "Configuring Prowl using YAML has been deprecated. An attempt to automatically migrate the existing configuration failed.\nPlease add the entry manually through the UI, remove the old YAML and update any automations to use the new `notify.send_message` action exposed with this new entity."
}
}
}
2 changes: 1 addition & 1 deletion tests/components/prowl/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async def configure_prowl_through_yaml(
{
NOTIFY_DOMAIN: [
{
"name": DOMAIN,
"name": TEST_NAME,
"platform": DOMAIN,
"api_key": TEST_API_KEY,
},
Expand Down
23 changes: 23 additions & 0 deletions tests/components/prowl/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Helpers for Prowl tests."""

from homeassistant.components.prowl.const import DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant


def get_config_entry(
hass: HomeAssistant,
api_key: str,
name: str | None = None,
config_method: str | None = None,
) -> ConfigEntry | None:
"""Get the Prowl config entry with the specified API key."""
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.data.get(CONF_API_KEY) == api_key:
if name and entry.title != name:
continue
if config_method and entry.source != config_method:
continue
return entry
return None
4 changes: 2 additions & 2 deletions tests/components/prowl/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def test_flow_duplicate_api_key(

async def test_flow_user_bad_key(hass: HomeAssistant, mock_prowlpy: AsyncMock) -> None:
"""Test user submitting a bad API key."""
mock_prowlpy.verify_key.side_effect = prowlpy.APIError("Invalid API key")
mock_prowlpy.verify_key.side_effect = prowlpy.InvalidAPIKeyError

result = await hass.config_entries.flow.async_init(
DOMAIN,
Expand Down Expand Up @@ -94,7 +94,7 @@ async def test_flow_user_prowl_timeout(

async def test_flow_api_failure(hass: HomeAssistant, mock_prowlpy: AsyncMock) -> None:
"""Test Prowl API failure."""
mock_prowlpy.verify_key.side_effect = prowlpy.APIError(BAD_API_RESPONSE)
mock_prowlpy.verify_key.side_effect = prowlpy.BadRequestError

result = await hass.config_entries.flow.async_init(
DOMAIN,
Expand Down
47 changes: 43 additions & 4 deletions tests/components/prowl/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
from homeassistant.components.prowl.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component

from .conftest import ENTITY_ID, TEST_API_KEY
from .conftest import ENTITY_ID, TEST_API_KEY, TEST_NAME, TEST_SERVICE

from tests.common import MockConfigEntry

Expand Down Expand Up @@ -44,11 +45,11 @@ async def test_load_reload_unload_config_entry(
[
(TimeoutError, ConfigEntryState.SETUP_RETRY),
(
prowlpy.APIError(f"Invalid API key: {TEST_API_KEY}"),
prowlpy.InvalidAPIKeyError(f"Invalid API key: {TEST_API_KEY}"),
ConfigEntryState.SETUP_ERROR,
),
(
prowlpy.APIError("Not accepted: exceeded rate limit"),
prowlpy.RateLimitExceededError("Not accepted: exceeded rate limit"),
ConfigEntryState.SETUP_RETRY,
),
(prowlpy.APIError("Internal server error"), ConfigEntryState.SETUP_ERROR),
Expand All @@ -72,6 +73,44 @@ async def test_config_entry_failures(
assert mock_prowlpy.verify_key.call_count > 0


@pytest.mark.parametrize(
("prowlpy_side_effect"),
[
(TimeoutError),
(prowlpy.InvalidAPIKeyError(f"Invalid API key: {TEST_API_KEY}")),
(prowlpy.RateLimitExceededError("Not accepted: exceeded rate limit")),
(prowlpy.APIError("Internal server error")),
],
)
async def test_yaml_entry_failures(
hass: HomeAssistant,
mock_prowlpy: AsyncMock,
prowlpy_side_effect,
) -> None:
"""Test the Prowl configuration entry dealing with bad API key."""
mock_prowlpy.verify_key.side_effect = prowlpy_side_effect

await async_setup_component(
hass,
notify.DOMAIN,
{
notify.DOMAIN: [
{
"name": TEST_NAME,
"platform": DOMAIN,
"api_key": TEST_API_KEY,
},
]
},
)
await hass.async_block_till_done()

assert hass.services.has_service(notify.DOMAIN, TEST_SERVICE), (
"YAML config did not create service"
)
assert mock_prowlpy.verify_key.call_count > 0


@pytest.mark.usefixtures("configure_prowl_through_yaml")
async def test_both_yaml_and_config_entry(
hass: HomeAssistant,
Expand All @@ -85,7 +124,7 @@ async def test_both_yaml_and_config_entry(
assert mock_prowlpy_config_entry.state is ConfigEntryState.LOADED

# Ensure we have the YAML entity service
assert hass.services.has_service(notify.DOMAIN, DOMAIN)
assert hass.services.has_service(notify.DOMAIN, TEST_SERVICE)

# Ensure we have the config entry entity service
assert hass.states.get(ENTITY_ID) is not None
Loading
Loading