"""Threshold calculator for suggesting trigger values based on recorder statistics.""" from __future__ import annotations import logging from dataclasses import dataclass from homeassistant.core import HomeAssistant from .entity_analyzer import EntityAnalysis, EntityAnalyzer, StatisticsInfo _LOGGER = logging.getLogger(__name__) @dataclass class ThresholdSuggestions: """Suggested threshold values for a trigger.""" current_value: float | None = None unit: str = "" average: float | None = None minimum: float | None = None maximum: float | None = None suggested_above: float | None = None suggested_below: float | None = None data_period_days: int = 0 percentile_10: float | None = None percentile_90: float | None = None trend: str | None = None class ThresholdCalculator: """Calculates intelligent threshold suggestions based on entity statistics.""" def __init__(self, hass: HomeAssistant) -> None: """Initialize the calculator.""" self.hass = hass async def async_calculate_suggestions( self, entity_id: str, attribute: str | None = None, analysis: EntityAnalysis | None = None, ) -> ThresholdSuggestions: """Generate threshold suggestions based on recorder statistics.""" state = self.hass.states.get(entity_id) if state is None: return ThresholdSuggestions() unit = state.attributes.get("unit_of_measurement", "") try: if attribute: current = float(state.attributes.get(attribute, 0)) else: current = float(state.state) except (ValueError, TypeError): return ThresholdSuggestions(unit=unit) # Fetch analysis if not provided if analysis is None: analyzer = EntityAnalyzer(self.hass) analysis = await analyzer.async_analyze_entity(entity_id) # Try statistics-based suggestions if analysis and analysis.statistics and analysis.statistics.has_data: stats = analysis.statistics return self._suggestions_from_statistics(current, unit, stats) # Fallback: naive calculation return self._naive_suggestions(current, unit) def _suggestions_from_statistics( self, current: float, unit: str, stats: StatisticsInfo, ) -> ThresholdSuggestions: """Calculate suggestions from recorder statistics.""" suggested_above = None suggested_below = None if stats.percentile_90 is not None: # Above: 20% above P90 (catches unusual highs) suggested_above = round(stats.percentile_90 * 1.2, 2) if stats.percentile_10 is not None: # Below: 20% below P10 (catches unusual lows) suggested_below = round(stats.percentile_10 * 0.8, 2) # Ensure suggestions don't cross each other or current value nonsensically if suggested_above is not None and suggested_below is not None: if suggested_above <= suggested_below: # Range too narrow, use wider margins if stats.mean is not None and stats.std_dev is not None: suggested_above = round(stats.mean + 2 * stats.std_dev, 2) suggested_below = round(stats.mean - 2 * stats.std_dev, 2) return ThresholdSuggestions( current_value=round(current, 2), unit=unit, average=stats.mean, minimum=stats.minimum, maximum=stats.maximum, suggested_above=suggested_above, suggested_below=suggested_below, data_period_days=stats.period_days, percentile_10=stats.percentile_10, percentile_90=stats.percentile_90, trend=stats.recent_trend, ) def _naive_suggestions(self, current: float, unit: str) -> ThresholdSuggestions: """Fallback suggestions when no statistics are available.""" if current > 0: suggested_above = round(current * 1.5, 2) suggested_below = round(current * 0.5, 2) else: suggested_above = round(current + 10, 2) suggested_below = round(current - 10, 2) return ThresholdSuggestions( current_value=round(current, 2), unit=unit, average=None, minimum=None, maximum=None, suggested_above=suggested_above, suggested_below=suggested_below, data_period_days=0, )