Skip to content

Implement Enhanced Error Handling with Repair Flows #7

Description

@adamjacobmuller

Implement Enhanced Error Handling with Repair Flows

🎯 Objective

Implement Home Assistant's repair flow system to provide actionable solutions for common TryFi integration issues. This will guide users through fixing problems without needing technical knowledge.

📋 Requirements

  • Implement repair flows for common issues
  • Provide clear, actionable fix instructions
  • Auto-detect problems and suggest repairs
  • Support both automatic and manual repairs
  • Integrate with Home Assistant's repair system

🔧 Implementation Plan

1. Create Repair Flow Handler

Create repairs.py

"""Repairs support for TryFi."""
from __future__ import annotations

import logging
from typing import Any

import voluptuous as vol

from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import issue_registry as ir

from .const import DOMAIN
from .pytryfi import PyTryFi

_LOGGER = logging.getLogger(__name__)

# Issue IDs
ISSUE_INVALID_AUTH = "invalid_authentication"
ISSUE_NO_PETS_FOUND = "no_pets_found"
ISSUE_BASE_OFFLINE = "base_station_offline"
ISSUE_COLLAR_NOT_RESPONDING = "collar_not_responding"
ISSUE_API_RATE_LIMITED = "api_rate_limited"
ISSUE_OLD_FIRMWARE = "outdated_firmware"


class TryFiRepairFlow(RepairsFlow):
    """Handler for TryFi repair flows."""
    
    def __init__(self, issue_id: str, data: dict[str, Any]) -> None:
        """Initialize the repair flow."""
        self.issue_id = issue_id
        self.data = data
        self.entry: ConfigEntry | None = None
    
    async def async_step_init(self) -> FlowResult:
        """Handle the initial step."""
        if self.issue_id == ISSUE_INVALID_AUTH:
            return await self.async_step_reauth()
        elif self.issue_id == ISSUE_NO_PETS_FOUND:
            return await self.async_step_no_pets()
        elif self.issue_id == ISSUE_BASE_OFFLINE:
            return await self.async_step_base_offline()
        elif self.issue_id == ISSUE_COLLAR_NOT_RESPONDING:
            return await self.async_step_collar_issue()
        elif self.issue_id == ISSUE_API_RATE_LIMITED:
            return await self.async_step_rate_limited()
        
        return self.async_show_form(step_id="init")
    
    async def async_step_reauth(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle re-authentication."""
        errors = {}
        
        if user_input is not None:
            try:
                # Test new credentials
                tryfi = await self.hass.async_add_executor_job(
                    PyTryFi,
                    user_input[CONF_USERNAME],
                    user_input[CONF_PASSWORD],
                )
                
                # Update config entry
                self.entry = self.hass.config_entries.async_get_entry(
                    self.data["entry_id"]
                )
                if self.entry:
                    self.hass.config_entries.async_update_entry(
                        self.entry,
                        data={**self.entry.data, **user_input},
                    )
                    
                    # Clear the issue
                    ir.async_delete_issue(
                        self.hass, DOMAIN, f"{ISSUE_INVALID_AUTH}_{self.entry.entry_id}"
                    )
                    
                    return self.async_create_entry(
                        title="",
                        data={},
                    )
            except Exception:
                errors["base"] = "invalid_auth"
        
        return self.async_show_form(
            step_id="reauth",
            data_schema=vol.Schema({
                vol.Required(CONF_USERNAME): str,
                vol.Required(CONF_PASSWORD): str,
            }),
            errors=errors,
            description_placeholders={
                "error": "Your TryFi credentials are no longer valid.",
            },
        )
    
    async def async_step_no_pets(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle no pets found issue."""
        if user_input is not None:
            # Just acknowledge and close
            ir.async_delete_issue(
                self.hass, DOMAIN, f"{ISSUE_NO_PETS_FOUND}_{self.data['entry_id']}"
            )
            return self.async_create_entry(title="", data={})
        
        return self.async_show_form(
            step_id="no_pets",
            description_placeholders={
                "instructions": (
                    "No pets with collars were found in your TryFi account.\n\n"
                    "Please:\n"
                    "1. Open the TryFi app\n"
                    "2. Ensure your pet has a collar assigned\n"
                    "3. Verify the collar is activated\n"
                    "4. Wait 5 minutes and reload this integration"
                ),
            },
        )
    
    async def async_step_base_offline(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle offline base station."""
        if user_input is not None:
            # Acknowledge the steps were taken
            ir.async_delete_issue(
                self.hass, DOMAIN, f"{ISSUE_BASE_OFFLINE}_{self.data['base_id']}"
            )
            return self.async_create_entry(title="", data={})
        
        return self.async_show_form(
            step_id="base_offline",
            description_placeholders={
                "base_name": self.data.get("base_name", "Unknown"),
                "instructions": (
                    f"The base station '{self.data.get('base_name', 'Unknown')}' is offline.\n\n"
                    "To fix this:\n"
                    "1. Check that the base station is plugged in\n"
                    "2. Verify the LED is solid blue (not blinking)\n"
                    "3. Check your WiFi is working\n"
                    "4. Try unplugging for 10 seconds and reconnecting\n"
                    "5. If still offline, use TryFi app to reconfigure WiFi"
                ),
            },
        )
    
    async def async_step_collar_issue(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle collar not responding."""
        if user_input is not None:
            ir.async_delete_issue(
                self.hass, DOMAIN, f"{ISSUE_COLLAR_NOT_RESPONDING}_{self.data['pet_id']}"
            )
            return self.async_create_entry(title="", data={})
        
        return self.async_show_form(
            step_id="collar_issue",
            description_placeholders={
                "pet_name": self.data.get("pet_name", "Unknown"),
                "instructions": (
                    f"{self.data.get('pet_name', 'Your pet')}'s collar is not responding.\n\n"
                    "Try these steps:\n"
                    "1. Ensure collar is charged (charge for 2 hours)\n"
                    "2. Check collar LED - should blink when pressed\n"
                    "3. Move pet near a base station\n"
                    "4. Restart collar: hold button for 10 seconds\n"
                    "5. If still not working, contact TryFi support"
                ),
            },
        )
    
    async def async_step_rate_limited(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle API rate limiting."""
        if user_input is not None:
            # Update polling rate
            new_rate = user_input.get("polling_rate", 30)
            self.entry = self.hass.config_entries.async_get_entry(
                self.data["entry_id"]
            )
            if self.entry:
                self.hass.config_entries.async_update_entry(
                    self.entry,
                    options={**self.entry.options, "polling_rate": new_rate},
                )
            
            ir.async_delete_issue(
                self.hass, DOMAIN, f"{ISSUE_API_RATE_LIMITED}_{self.data['entry_id']}"
            )
            return self.async_create_entry(title="", data={})
        
        return self.async_show_form(
            step_id="rate_limited",
            data_schema=vol.Schema({
                vol.Optional("polling_rate", default=30): vol.All(
                    vol.Coerce(int),
                    vol.Range(min=10, max=300),
                ),
            }),
            description_placeholders={
                "current_rate": str(self.data.get("current_rate", 10)),
                "instructions": (
                    "TryFi API is rate limiting requests.\n\n"
                    f"Current polling rate: {self.data.get('current_rate', 10)} seconds\n"
                    "Recommended: 30 seconds or higher\n\n"
                    "Increase the polling interval to reduce API calls."
                ),
            },
        )


async def async_create_fix_flow(
    hass: HomeAssistant,
    issue_id: str,
    data: dict[str, Any] | None = None,
) -> RepairsFlow:
    """Create a repair flow."""
    return TryFiRepairFlow(issue_id, data or {})

2. Create Issue Detection

Update __init__.py to detect and create issues:

# Add to coordinator's _async_update_data method

async def _async_update_data(self) -> PyTryFi:
    """Fetch data from TryFi API."""
    try:
        await self.hass.async_add_executor_job(self.tryfi.update)
        
        # Check for issues after successful update
        await self._check_for_issues()
        
    except InvalidAuthError as err:
        # Create repair issue for auth failure
        ir.async_create_issue(
            self.hass,
            DOMAIN,
            f"{ISSUE_INVALID_AUTH}_{self.config_entry.entry_id}",
            is_fixable=True,
            severity=ir.IssueSeverity.ERROR,
            translation_key="invalid_authentication",
            data={"entry_id": self.config_entry.entry_id},
        )
        raise UpdateFailed("Invalid authentication") from err

async def _check_for_issues(self) -> None:
    """Check for issues that need repair."""
    # Check for no pets
    if not self.tryfi.pets:
        ir.async_create_issue(
            self.hass,
            DOMAIN,
            f"{ISSUE_NO_PETS_FOUND}_{self.config_entry.entry_id}",
            is_fixable=True,
            severity=ir.IssueSeverity.WARNING,
            translation_key="no_pets_found",
            data={"entry_id": self.config_entry.entry_id},
        )
    else:
        # Clear if pets found
        ir.async_delete_issue(
            self.hass,
            DOMAIN,
            f"{ISSUE_NO_PETS_FOUND}_{self.config_entry.entry_id}",
        )
    
    # Check for offline bases
    for base in self.tryfi.bases:
        if not base.online:
            ir.async_create_issue(
                self.hass,
                DOMAIN,
                f"{ISSUE_BASE_OFFLINE}_{base.baseId}",
                is_fixable=True,
                severity=ir.IssueSeverity.WARNING,
                translation_key="base_station_offline",
                data={
                    "base_id": base.baseId,
                    "base_name": base.name,
                },
            )
        else:
            ir.async_delete_issue(
                self.hass,
                DOMAIN,
                f"{ISSUE_BASE_OFFLINE}_{base.baseId}",
            )
    
    # Check for collar issues
    for pet in self.tryfi.pets:
        if pet.device and hasattr(pet.device, "connectionStateType"):
            # Check if collar hasn't connected in 24 hours
            last_connection = getattr(pet.device, "connectionStateDate", None)
            if last_connection:
                time_since = datetime.now() - last_connection
                if time_since.days >= 1:
                    ir.async_create_issue(
                        self.hass,
                        DOMAIN,
                        f"{ISSUE_COLLAR_NOT_RESPONDING}_{pet.petId}",
                        is_fixable=True,
                        severity=ir.IssueSeverity.WARNING,
                        translation_key="collar_not_responding",
                        data={
                            "pet_id": pet.petId,
                            "pet_name": pet.name,
                        },
                    )
                else:
                    ir.async_delete_issue(
                        self.hass,
                        DOMAIN,
                        f"{ISSUE_COLLAR_NOT_RESPONDING}_{pet.petId}",
                    )

3. Update strings.json for Repairs

Add repair translations:

{
  "issues": {
    "invalid_authentication": {
      "title": "TryFi Authentication Failed",
      "description": "Your TryFi login credentials are no longer valid. Please re-enter your credentials to fix this issue."
    },
    "no_pets_found": {
      "title": "No Pets Found",
      "description": "No pets with collars were found in your TryFi account. Please ensure your pets have collars assigned in the TryFi app."
    },
    "base_station_offline": {
      "title": "Base Station Offline: {base_name}",
      "description": "The base station '{base_name}' is offline and cannot communicate with collars."
    },
    "collar_not_responding": {
      "title": "Collar Not Responding: {pet_name}",
      "description": "{pet_name}'s collar hasn't connected in over 24 hours."
    },
    "api_rate_limited": {
      "title": "TryFi API Rate Limited",
      "description": "The TryFi API is rate limiting requests. Consider increasing the polling interval."
    }
  }
}

4. Register Repair Flow

Update __init__.py:

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up TryFi from a config entry."""
    # ... existing setup code ...
    
    # Register repairs
    await repairs.async_setup(hass)
    
    return True

📝 Checklist

  • Create repairs.py with repair flow handler
  • Implement repair flows for each issue type
  • Add issue detection in coordinator
  • Create automatic issue creation/deletion
  • Add repair flow translations
  • Implement re-authentication flow
  • Add rate limit adjustment flow
  • Create troubleshooting instructions
  • Test each repair flow
  • Add repair flow tests
  • Document repair features
  • Add user notifications for critical issues

🧪 Testing

  1. Test each repair flow manually
  2. Verify issues appear in Settings → Repairs
  3. Test fix flows complete successfully
  4. Verify issues clear after fixes
  5. Test with various error conditions
  6. Check translations work properly

📚 Resources

⏱️ Estimated Time

  • Implementation: 12 hours
  • Testing: 4 hours
  • Documentation: 2 hours

🏷️ Labels

enhancement, platinum-tier, repairs, user-experience

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestplatinumTasks required to get this integration to platinum status

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions