From 14f0aa3303511c234bf3f52d1bcbdd4145c09cf0 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 5 Nov 2024 11:10:38 +0000 Subject: [PATCH] Repairs (#2293) * WIP * WIP * WIP * WIP * Remove print statement * Change logging * Lint * Error handling * Error handling * Error handling for entities * Remove issues if device deleted --- custom_components/battery_notes/__init__.py | 4 ++ .../battery_notes/config_flow.py | 38 ++++++++---- custom_components/battery_notes/device.py | 59 +++++++++++++++++-- custom_components/battery_notes/repairs.py | 57 ++++++++++++++++++ .../battery_notes/translations/en.json | 14 +++++ 5 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 custom_components/battery_notes/repairs.py diff --git a/custom_components/battery_notes/__init__.py b/custom_components/battery_notes/__init__.py index 4aaa385cb..6925d67d7 100644 --- a/custom_components/battery_notes/__init__.py +++ b/custom_components/battery_notes/__init__.py @@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -163,6 +164,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Device removed, tidy up store.""" + # Remove any issues raised + ir.async_delete_issue(hass, DOMAIN, f"missing_device_{config_entry.entry_id}") + if "device_id" not in config_entry.data: return diff --git a/custom_components/battery_notes/config_flow.py b/custom_components/battery_notes/config_flow.py index eaa4b402a..3816555e3 100644 --- a/custom_components/battery_notes/config_flow.py +++ b/custom_components/battery_notes/config_flow.py @@ -448,20 +448,23 @@ async def async_step_init( device_registry = dr.async_get(self.hass) device_entry = device_registry.async_get(self.source_device_id) - _LOGGER.debug( - "Looking up device %s %s %s %s", - device_entry.manufacturer, - device_entry.model, - get_device_model_id(device_entry) or "", - device_entry.hw_version, - ) + if not device_entry: + errors["base"] = "orphaned_battery_note" + else: + _LOGGER.debug( + "Looking up device %s %s %s %s", + device_entry.manufacturer, + device_entry.model, + get_device_model_id(device_entry) or "", + device_entry.hw_version, + ) - self.model_info = ModelInfo( - device_entry.manufacturer, - device_entry.model, - get_device_model_id(device_entry), - device_entry.hw_version, - ) + self.model_info = ModelInfo( + device_entry.manufacturer, + device_entry.model, + get_device_model_id(device_entry), + device_entry.hw_version, + ) schema = self.build_options_schema() if user_input is not None: @@ -492,6 +495,8 @@ async def save_options( schema: vol.Schema, ) -> dict: """Save options, and return errors when validation fails.""" + errors = {} + device_registry = dr.async_get(self.hass) device_entry = device_registry.async_get( self.config_entry.data.get(CONF_DEVICE_ID) @@ -502,6 +507,13 @@ async def save_options( if source_entity_id: entity_registry = er.async_get(self.hass) entity_entry = entity_registry.async_get(source_entity_id) + if not entity_entry: + errors["base"] = "orphaned_battery_note" + return errors + else: + if not device_entry: + errors["base"] = "orphaned_battery_note" + return errors if CONF_NAME in user_input: title = user_input.get(CONF_NAME) diff --git a/custom_components/battery_notes/device.py b/custom_components/battery_notes/device.py index 70540bb6d..2e02c007f 100644 --- a/custom_components/battery_notes/device.py +++ b/custom_components/battery_notes/device.py @@ -13,12 +13,9 @@ PERCENTAGE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.helpers import ( - device_registry as dr, -) -from homeassistant.helpers import ( - entity_registry as er, -) +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.entity_registry import RegistryEntry from .const import ( @@ -94,6 +91,32 @@ async def async_setup(self) -> bool: if source_entity_id: entity = entity_registry.async_get(source_entity_id) + + if not entity: + ir.async_create_issue( + self.hass, + DOMAIN, + f"missing_device_{self.config.entry_id}", + data={ + "entry_id": self.config.entry_id, + "device_id": device_id, + "source_entity_id": source_entity_id, + }, + is_fixable=True, + severity=ir.IssueSeverity.WARNING, + translation_key="missing_device", + translation_placeholders={ + "name": config.title, + }, + ) + + _LOGGER.warning( + "%s is orphaned, unable to find entity %s", + self.config.entry_id, + source_entity_id, + ) + return False + device_class = entity.device_class or entity.original_device_class if ( device_class == SensorDeviceClass.BATTERY @@ -150,6 +173,30 @@ async def async_setup(self) -> bool: else: self.device_name = self.config.title + ir.async_create_issue( + self.hass, + DOMAIN, + f"missing_device_{self.config.entry_id}", + data={ + "entry_id": self.config.entry_id, + "device_id": device_id, + "source_entity_id": source_entity_id, + }, + is_fixable=True, + severity=ir.IssueSeverity.WARNING, + translation_key="missing_device", + translation_placeholders={ + "name": config.title, + }, + ) + + _LOGGER.warning( + "%s is orphaned, unable to find device %s", + self.config.entry_id, + device_id, + ) + return False + self.store = self.hass.data[DOMAIN][DATA_STORE] self.coordinator = BatteryNotesCoordinator( self.hass, self.store, self.wrapped_battery diff --git a/custom_components/battery_notes/repairs.py b/custom_components/battery_notes/repairs.py new file mode 100644 index 000000000..4cdfb1bda --- /dev/null +++ b/custom_components/battery_notes/repairs.py @@ -0,0 +1,57 @@ +"""Repairs for battery_notes.""" + +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 MissingDeviceRepairFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, data: dict[str, str]) -> None: + """Initialize.""" + self.entry_id = data["entry_id"] + self.device_id = data["device_id"] + self.source_entity_id = data["source_entity_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: + await self.hass.config_entries.async_remove(self.entry_id) + + return self.async_create_entry(title="", 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.""" + if issue_id.startswith("missing_device_"): + assert data + return MissingDeviceRepairFlow(data) diff --git a/custom_components/battery_notes/translations/en.json b/custom_components/battery_notes/translations/en.json index b2476d695..e44639658 100644 --- a/custom_components/battery_notes/translations/en.json +++ b/custom_components/battery_notes/translations/en.json @@ -75,6 +75,7 @@ } }, "error": { + "orphaned_battery_note": "The associated device or entity no longer exists for this Battery Note.", "unknown": "Unknown error occurred." } }, @@ -182,5 +183,18 @@ "description": "Raise events for devices that have a low battery.", "name": "Check battery low" } + }, + "issues": { + "missing_device": { + "title": "Orphaned Battery Note", + "fix_flow": { + "step": { + "confirm": { + "title": "Orphaned Battery Note", + "description": "The associated device or entity no longer exists for the Battery Note entry {name}, the Battery Note should be deleted.\nSelect **Submit** to delete this Battery Note." + } + } + } + } } } \ No newline at end of file