Update doorbird error notification to be a repair flow (#122987)

This commit is contained in:
J. Nick Koston 2024-08-01 01:31:22 -05:00 committed by GitHub
parent 352f0953f3
commit 8375b58eac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 153 additions and 15 deletions

View file

@ -3,11 +3,11 @@
from __future__ import annotations from __future__ import annotations
from http import HTTPStatus from http import HTTPStatus
import logging
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from doorbirdpy import DoorBird from doorbirdpy import DoorBird
from homeassistant.components import persistent_notification
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
@ -17,6 +17,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType 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) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the DoorBird component.""" """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_bird_data = DoorBirdData(door_station, info, event_entity_ids)
door_station.update_events(events) door_station.update_events(events)
# Subscribe to doorbell or motion 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 raise ConfigEntryNotReady
entry.async_on_unload(entry.add_update_listener(_update_listener)) 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( async def _async_register_events(
hass: HomeAssistant, door_station: ConfiguredDoorBird hass: HomeAssistant, door_station: ConfiguredDoorBird, entry: DoorBirdConfigEntry
) -> bool: ) -> bool:
"""Register events on device.""" """Register events on device."""
issue_id = f"doorbird_schedule_error_{entry.entry_id}"
try: try:
await door_station.async_register_events() await door_station.async_register_events()
except ClientResponseError: except ClientResponseError as ex:
persistent_notification.async_create( ir.async_create_issue(
hass, hass,
( DOMAIN,
"Doorbird configuration failed. Please verify that API " issue_id,
"Operator permission is enabled for the Doorbird user. " severity=ir.IssueSeverity.ERROR,
"A restart will be required once permissions have been " translation_key="error_registering_events",
"verified." data={"entry_id": entry.entry_id},
), is_fixable=True,
title="Doorbird Configuration Failure", translation_placeholders={
notification_id="doorbird_schedule_error", "error": str(ex),
"name": door_station.name or entry.data[CONF_NAME],
},
) )
_LOGGER.debug("Error registering DoorBird events", exc_info=True)
return False return False
else:
ir.async_delete_issue(hass, DOMAIN, issue_id)
return True return True
@ -111,4 +120,4 @@ async def _update_listener(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> N
door_station = entry.runtime_data.door_station door_station = entry.runtime_data.door_station
door_station.update_events(entry.options[CONF_EVENTS]) door_station.update_events(entry.options[CONF_EVENTS])
# Subscribe to doorbell or motion events # Subscribe to doorbell or motion events
await _async_register_events(hass, door_station) await _async_register_events(hass, door_station, entry)

View file

@ -3,7 +3,7 @@
"name": "DoorBird", "name": "DoorBird",
"codeowners": ["@oblogic7", "@bdraco", "@flacjacket"], "codeowners": ["@oblogic7", "@bdraco", "@flacjacket"],
"config_flow": true, "config_flow": true,
"dependencies": ["http"], "dependencies": ["http", "repairs"],
"documentation": "https://www.home-assistant.io/integrations/doorbird", "documentation": "https://www.home-assistant.io/integrations/doorbird",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["doorbirdpy"], "loggers": ["doorbirdpy"],

View file

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

View file

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

View file

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