hass.tibber_prices/custom_components/tibber_prices/config_flow/validators.py
Julian Pawlowski d90266e1ad refactor(config_flow): split monolithic file into modular package structure
Refactored config_flow.py (995 lines) into focused modules within config_flow/
package to improve maintainability and code organization.

Changes:
- Created config_flow/ package with 6 specialized modules (1,260 lines total)
- Extracted validators to validators.py (95 lines) - pure, testable functions
- Extracted schemas to schemas.py (577 lines) - centralized vol.Schema definitions
- Split flow handlers into separate files:
  * user_flow.py (274 lines) - Main config flow (setup + reauth)
  * subentry_flow.py (124 lines) - Subentry flow (add homes)
  * options_flow.py (160 lines) - Options flow (6-step configuration wizard)
- Package exports via __init__.py (50 lines) for backward compatibility
- Deleted config_flow_legacy.py (no longer needed)

Technical improvements:
- Used Mapping[str, Any] for config_entry.options compatibility
- Proper TYPE_CHECKING imports for circular dependency management
- All 10 inline vol.Schema definitions replaced with reusable functions
- Validators are pure functions (no side effects, easily testable)
- Clear separation of concerns (validation, schemas, flows)

Documentation:
- Updated AGENTS.md with new package structure
- Updated config flow patterns and examples
- Added "Add a new config flow step" guide to Common Tasks
- Marked refactoring plan as COMPLETED with lessons learned

Verification:
- All linting checks pass (./scripts/lint-check)
- All flow handlers import successfully
- Home Assistant loads integration without errors
- All flow types functional (user, subentry, options, reauth)
- No user-facing changes (backward compatible)

Impact: Improves code maintainability by organizing 995 lines into 6 focused
modules (avg 210 lines/module). Enables easier testing, future modifications,
and onboarding of new contributors.
2025-11-15 13:03:13 +00:00

122 lines
3.1 KiB
Python

"""Validation functions for Tibber Prices config flow."""
from __future__ import annotations
from typing import TYPE_CHECKING
from custom_components.tibber_prices.api import (
TibberPricesApiClient,
TibberPricesApiClientAuthenticationError,
TibberPricesApiClientCommunicationError,
TibberPricesApiClientError,
)
from custom_components.tibber_prices.const import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.loader import async_get_integration
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
# Constants for validation
MAX_FLEX_PERCENTAGE = 100.0
MAX_MIN_PERIODS = 10 # Arbitrary upper limit for sanity
class InvalidAuthError(HomeAssistantError):
"""Error to indicate invalid authentication."""
class CannotConnectError(HomeAssistantError):
"""Error to indicate we cannot connect."""
async def validate_api_token(hass: HomeAssistant, token: str) -> dict:
"""
Validate Tibber API token.
Args:
hass: Home Assistant instance
token: Tibber API access token
Returns:
dict with viewer data on success
Raises:
InvalidAuthError: Invalid token
CannotConnectError: API connection failed
"""
try:
integration = await async_get_integration(hass, DOMAIN)
client = TibberPricesApiClient(
access_token=token,
session=async_create_clientsession(hass),
version=str(integration.version) if integration.version else "unknown",
)
result = await client.async_get_viewer_details()
return result["viewer"]
except TibberPricesApiClientAuthenticationError as exception:
raise InvalidAuthError from exception
except TibberPricesApiClientCommunicationError as exception:
raise CannotConnectError from exception
except TibberPricesApiClientError as exception:
raise CannotConnectError from exception
def validate_threshold_range(value: float, min_val: float, max_val: float) -> bool:
"""
Validate threshold is within allowed range.
Args:
value: Value to validate
min_val: Minimum allowed value
max_val: Maximum allowed value
Returns:
True if value is within range
"""
return min_val <= value <= max_val
def validate_period_length(minutes: int) -> bool:
"""
Validate period length is multiple of 15 minutes.
Args:
minutes: Period length in minutes
Returns:
True if length is valid
"""
return minutes > 0 and minutes % 15 == 0
def validate_flex_percentage(flex: float) -> bool:
"""
Validate flexibility percentage is within bounds.
Args:
flex: Flexibility percentage
Returns:
True if percentage is valid
"""
return 0.0 <= flex <= MAX_FLEX_PERCENTAGE
def validate_min_periods(count: int) -> bool:
"""
Validate minimum periods count is reasonable.
Args:
count: Number of minimum periods
Returns:
True if count is valid
"""
return count > 0 and count <= MAX_MIN_PERIODS