1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-13 17:44:45 +01:00

Add repair to unsubscribe protected topic in ntfy integration (#152009)

This commit is contained in:
Manu
2025-09-11 18:52:12 +02:00
committed by GitHub
parent b1a6e403fb
commit 9cfdb99e76
5 changed files with 143 additions and 5 deletions
+19 -1
View File
@@ -17,9 +17,17 @@ from aiontfy.exceptions import (
from homeassistant.components.event import EventEntity, EventEntityDescription
from homeassistant.config_entries import ConfigSubentry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_MESSAGE, CONF_PRIORITY, CONF_TAGS, CONF_TITLE
from .const import (
CONF_MESSAGE,
CONF_PRIORITY,
CONF_TAGS,
CONF_TITLE,
CONF_TOPIC,
DOMAIN,
)
from .coordinator import NtfyConfigEntry
from .entity import NtfyBaseEntity
@@ -100,6 +108,16 @@ class NtfyEventEntity(NtfyBaseEntity, EventEntity):
if self._attr_available:
_LOGGER.error("Failed to subscribe to topic. Topic is protected")
self._attr_available = False
ir.async_create_issue(
self.hass,
DOMAIN,
f"topic_protected_{self.topic}",
is_fixable=True,
severity=ir.IssueSeverity.ERROR,
translation_key="topic_protected",
translation_placeholders={CONF_TOPIC: self.topic},
data={"entity_id": self.entity_id, "topic": self.topic},
)
return
except NtfyHTTPError as e:
if self._attr_available:
@@ -63,9 +63,7 @@ rules:
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues:
status: exempt
comment: the integration has no repairs
repair-issues: done
stale-devices:
status: exempt
comment: only one device per entry, is deleted with the entry.
+56
View File
@@ -0,0 +1,56 @@
"""Repairs for ntfy integration."""
from __future__ import annotations
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .const import CONF_TOPIC
class TopicProtectedRepairFlow(RepairsFlow):
"""Handler for protected topic issue fixing flow."""
def __init__(self, data: dict[str, str]) -> None:
"""Initialize."""
self.entity_id = data["entity_id"]
self.topic = data["topic"]
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Init repair flow."""
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Confirm repair flow."""
if user_input is not None:
er.async_get(self.hass).async_update_entity(
self.entity_id,
disabled_by=er.RegistryEntryDisabler.USER,
)
return self.async_create_entry(data={})
return self.async_show_form(
step_id="confirm",
data_schema=vol.Schema({}),
description_placeholders={CONF_TOPIC: self.topic},
)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str],
) -> RepairsFlow:
"""Create flow."""
if issue_id.startswith("topic_protected"):
return TopicProtectedRepairFlow(data)
return ConfirmRepairFlow()
@@ -354,5 +354,18 @@
"5": "Maximum"
}
}
},
"issues": {
"topic_protected": {
"title": "Subscription failed: Topic {topic} is protected",
"fix_flow": {
"step": {
"confirm": {
"title": "Topic {topic} is protected",
"description": "The topic **{topic}** is protected and requires authentication to subscribe.\n\nTo resolve this issue, you have two options:\n\n1. **Reconfigure the ntfy integration**\nAdd a username and password that has permission to access this topic.\n\n2. **Deactivate the event entity**\nThis will stop Home Assistant from subscribing to the topic.\nClick **Submit** to deactivate the entity."
}
}
}
}
}
}
+54 -1
View File
@@ -17,12 +17,20 @@ from freezegun.api import FrozenDateTimeFactory, freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.ntfy.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.components.repairs import (
async_process_repairs_platforms,
process_repair_fix_flow,
start_repair_fix_flow,
)
from tests.typing import ClientSessionGenerator
@pytest.fixture(autouse=True)
@@ -156,3 +164,48 @@ async def test_event_exceptions(
assert (state := hass.states.get("event.mytopic"))
assert state.state == expected_state
async def test_event_topic_protected(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_aiontfy: AsyncMock,
freezer: FrozenDateTimeFactory,
issue_registry: ir.IssueRegistry,
entity_registry: er.EntityRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test ntfy events cannot subscribe to protected topic."""
mock_aiontfy.subscribe.side_effect = NtfyForbiddenError(403, 403, "forbidden")
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, "repairs", {})
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
freezer.tick(timedelta(seconds=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get("event.mytopic"))
assert state.state == STATE_UNAVAILABLE
assert issue_registry.async_get_issue(
domain=DOMAIN, issue_id="topic_protected_mytopic"
)
await async_process_repairs_platforms(hass)
client = await hass_client()
result = await start_repair_fix_flow(client, DOMAIN, "topic_protected_mytopic")
flow_id = result["flow_id"]
assert result["step_id"] == "confirm"
result = await process_repair_fix_flow(client, flow_id)
assert result["type"] == "create_entry"
assert (entity := entity_registry.async_get("event.mytopic"))
assert entity.disabled
assert entity.disabled_by is er.RegistryEntryDisabler.USER