From cb37ae660843142bc8b951c3b91ccd0c5739863f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Aug 2024 01:31:22 -0500 Subject: [PATCH] Update doorbird error notification to be a repair flow (#122987) --- homeassistant/components/doorbird/__init__.py | 37 ++++++----- .../components/doorbird/manifest.json | 2 +- homeassistant/components/doorbird/repairs.py | 55 +++++++++++++++++ .../components/doorbird/strings.json | 13 ++++ tests/components/doorbird/test_repairs.py | 61 +++++++++++++++++++ 5 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/doorbird/repairs.py create mode 100644 tests/components/doorbird/test_repairs.py diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8989e0ec0be..113b8031d9b 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -3,11 +3,11 @@ from __future__ import annotations from http import HTTPStatus +import logging from aiohttp import ClientResponseError from doorbirdpy import DoorBird -from homeassistant.components import persistent_notification from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -30,6 +31,8 @@ CONF_CUSTOM_URL = "hass_url_override" CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the DoorBird component.""" @@ -68,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> door_bird_data = DoorBirdData(door_station, info, event_entity_ids) door_station.update_events(events) # Subscribe to doorbell or motion events - if not await _async_register_events(hass, door_station): + if not await _async_register_events(hass, door_station, entry): raise ConfigEntryNotReady entry.async_on_unload(entry.add_update_listener(_update_listener)) @@ -84,24 +87,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> async def _async_register_events( - hass: HomeAssistant, door_station: ConfiguredDoorBird + hass: HomeAssistant, door_station: ConfiguredDoorBird, entry: DoorBirdConfigEntry ) -> bool: """Register events on device.""" + issue_id = f"doorbird_schedule_error_{entry.entry_id}" try: await door_station.async_register_events() - except ClientResponseError: - persistent_notification.async_create( + except ClientResponseError as ex: + ir.async_create_issue( hass, - ( - "Doorbird configuration failed. Please verify that API " - "Operator permission is enabled for the Doorbird user. " - "A restart will be required once permissions have been " - "verified." - ), - title="Doorbird Configuration Failure", - notification_id="doorbird_schedule_error", + DOMAIN, + issue_id, + severity=ir.IssueSeverity.ERROR, + translation_key="error_registering_events", + data={"entry_id": entry.entry_id}, + is_fixable=True, + translation_placeholders={ + "error": str(ex), + "name": door_station.name or entry.data[CONF_NAME], + }, ) + _LOGGER.debug("Error registering DoorBird events", exc_info=True) return False + else: + ir.async_delete_issue(hass, DOMAIN, issue_id) return True @@ -111,4 +120,4 @@ async def _update_listener(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> N door_station = entry.runtime_data.door_station door_station.update_events(entry.options[CONF_EVENTS]) # Subscribe to doorbell or motion events - await _async_register_events(hass, door_station) + await _async_register_events(hass, door_station, entry) diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index e77f9aaf0a4..0e9f03c8ef8 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -3,7 +3,7 @@ "name": "DoorBird", "codeowners": ["@oblogic7", "@bdraco", "@flacjacket"], "config_flow": true, - "dependencies": ["http"], + "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/doorbird", "iot_class": "local_push", "loggers": ["doorbirdpy"], diff --git a/homeassistant/components/doorbird/repairs.py b/homeassistant/components/doorbird/repairs.py new file mode 100644 index 00000000000..c8f9b73ecbd --- /dev/null +++ b/homeassistant/components/doorbird/repairs.py @@ -0,0 +1,55 @@ +"""Repairs for DoorBird.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + + +class DoorBirdReloadConfirmRepairFlow(RepairsFlow): + """Handler to show doorbird error and reload.""" + + def __init__(self, entry_id: str) -> None: + """Initialize the flow.""" + self.entry_id = entry_id + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + self.hass.config_entries.async_schedule_reload(self.entry_id) + return self.async_create_entry(data={}) + + issue_registry = ir.async_get(self.hass) + description_placeholders = None + if issue := issue_registry.async_get_issue(self.handler, self.issue_id): + description_placeholders = issue.translation_placeholders + + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=description_placeholders, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + assert data is not None + entry_id = data["entry_id"] + assert isinstance(entry_id, str) + return DoorBirdReloadConfirmRepairFlow(entry_id=entry_id) diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 29c85ec7311..090ba4f161f 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -11,6 +11,19 @@ } } }, + "issues": { + "error_registering_events": { + "title": "DoorBird {name} configuration failure", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::doorbird::issues::error_registering_events::title%]", + "description": "Configuring DoorBird {name} failed with error: `{error}`. Please enable the API Operator permission for the DoorBird user and continue to reload the integration." + } + } + } + } + }, "config": { "step": { "user": { diff --git a/tests/components/doorbird/test_repairs.py b/tests/components/doorbird/test_repairs.py new file mode 100644 index 00000000000..7449250b718 --- /dev/null +++ b/tests/components/doorbird/test_repairs.py @@ -0,0 +1,61 @@ +"""Test repairs for doorbird.""" + +from __future__ import annotations + +from http import HTTPStatus + +from homeassistant.components.doorbird.const import DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from . import mock_not_found_exception +from .conftest import DoorbirdMockerType + +from tests.typing import ClientSessionGenerator + + +async def test_change_schedule_fails( + hass: HomeAssistant, + doorbird_mocker: DoorbirdMockerType, + hass_client: ClientSessionGenerator, +) -> None: + """Test a doorbird when change_schedule fails.""" + assert await async_setup_component(hass, "repairs", {}) + doorbird_entry = await doorbird_mocker( + favorites_side_effect=mock_not_found_exception() + ) + assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY + issue_reg = ir.async_get(hass) + assert len(issue_reg.issues) == 1 + issue = list(issue_reg.issues.values())[0] + issue_id = issue.issue_id + assert issue.domain == DOMAIN + + await async_process_repairs_platforms(hass) + client = await hass_client() + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + placeholders = data["description_placeholders"] + assert "404" in placeholders["error"] + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry"