New apps Added
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
"""Prediction service for calculating heating times."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ..constants import (
|
||||
BASE_HIGH_CONFIDENCE,
|
||||
BASE_LOW_CONFIDENCE,
|
||||
BASE_MEDIUM_CONFIDENCE,
|
||||
CLOUD_COVERAGE_FACTOR,
|
||||
CONFIDENCE_BOOST_PER_SENSOR,
|
||||
DEFAULT_ANTICIPATION_BUFFER,
|
||||
HIGH_CONFIDENCE_SLOPE,
|
||||
HUMIDITY_FACTOR,
|
||||
HUMIDITY_REFERENCE,
|
||||
MAX_ANTICIPATION_TIME,
|
||||
MEDIUM_CONFIDENCE_SLOPE,
|
||||
MIN_ANTICIPATION_TIME,
|
||||
OUTDOOR_TEMP_FACTOR,
|
||||
OUTDOOR_TEMP_REFERENCE,
|
||||
)
|
||||
from ..value_objects import PredictionResult
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PredictionService:
|
||||
"""Service for predicting heating start times.
|
||||
|
||||
This service contains the core prediction algorithm that determines
|
||||
when heating should start to reach target temperature at a scheduled time.
|
||||
|
||||
The calculation considers:
|
||||
1. Temperature difference to heat
|
||||
2. Outdoor temperature impact on heat loss
|
||||
3. Humidity effects on heating efficiency
|
||||
4. Solar gain from cloud coverage
|
||||
5. Learned heating slope (heating rate) from historical data
|
||||
"""
|
||||
|
||||
def predict_heating_time(
|
||||
self,
|
||||
current_temp: float | None,
|
||||
target_temp: float,
|
||||
learned_slope: float,
|
||||
target_time: datetime,
|
||||
outdoor_temp: float | None = None,
|
||||
humidity: float | None = None,
|
||||
cloud_coverage: float | None = None,
|
||||
dead_time_minutes: float = 0.0,
|
||||
) -> PredictionResult:
|
||||
"""Calculate when heating should start.
|
||||
|
||||
Args:
|
||||
current_temp: Current room temperature in Celsius (None = cannot calculate)
|
||||
target_temp: Target temperature in Celsius
|
||||
learned_slope: Learned heating slope in °C/hour
|
||||
target_time: When target should be reached (mandatory)
|
||||
outdoor_temp: Outdoor temperature in Celsius (optional)
|
||||
humidity: Indoor humidity percentage (0-100) (optional)
|
||||
cloud_coverage: Cloud coverage percentage (0-100, 0=clear sky) (optional)
|
||||
dead_time_minutes: Dead time in minutes (initial period with minimal heating effect)
|
||||
|
||||
Returns:
|
||||
Prediction result with start time and confidence
|
||||
"""
|
||||
# Handle missing current temperature
|
||||
if current_temp is None:
|
||||
_LOGGER.warning("Cannot calculate prediction: current_temp is None")
|
||||
return PredictionResult(
|
||||
anticipated_start_time=target_time,
|
||||
estimated_duration_minutes=0.0,
|
||||
confidence_level=0.0,
|
||||
learned_heating_slope=learned_slope,
|
||||
)
|
||||
|
||||
# Calculate temperature difference
|
||||
temp_delta = target_temp - current_temp
|
||||
|
||||
if temp_delta <= 0:
|
||||
# Already at target, anticipated start time = target time
|
||||
_LOGGER.debug(
|
||||
"Already at target temperature (%.1f°C >= %.1f°C), no heating needed",
|
||||
current_temp,
|
||||
target_temp,
|
||||
)
|
||||
return PredictionResult(
|
||||
anticipated_start_time=target_time,
|
||||
estimated_duration_minutes=0.0,
|
||||
confidence_level=1.0,
|
||||
learned_heating_slope=learned_slope,
|
||||
)
|
||||
|
||||
# Protection against invalid slope (should not happen with proper validation)
|
||||
if learned_slope is None or learned_slope <= 0:
|
||||
_LOGGER.error(
|
||||
"CRITICAL: Invalid learned heating slope (%.4f°C/h <= 0) reached prediction_service. "
|
||||
"This indicates missing validation in calling code. Cannot calculate prediction.",
|
||||
learned_slope or 0,
|
||||
)
|
||||
return PredictionResult(
|
||||
anticipated_start_time=target_time,
|
||||
estimated_duration_minutes=0.0,
|
||||
confidence_level=0.0,
|
||||
learned_heating_slope=learned_slope or 0,
|
||||
)
|
||||
|
||||
# Calculate base anticipation time (in minutes)
|
||||
# Formula: heating_time = dead_time + (temp_delta / slope) * 60
|
||||
anticipation_minutes = dead_time_minutes + (temp_delta / learned_slope) * 60.0
|
||||
# Apply environmental correction factors
|
||||
correction_factor = self._calculate_environmental_correction(
|
||||
outdoor_temp, humidity, cloud_coverage
|
||||
)
|
||||
|
||||
anticipation_minutes *= correction_factor
|
||||
|
||||
# Apply buffer and limits
|
||||
anticipation_minutes += DEFAULT_ANTICIPATION_BUFFER
|
||||
anticipation_minutes = max(
|
||||
MIN_ANTICIPATION_TIME, min(MAX_ANTICIPATION_TIME, anticipation_minutes)
|
||||
)
|
||||
|
||||
# Calculate anticipated start time
|
||||
anticipated_start = target_time - timedelta(minutes=anticipation_minutes)
|
||||
|
||||
# Calculate confidence level based on slope and available environmental data
|
||||
confidence = self._calculate_confidence(learned_slope, outdoor_temp, humidity)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Prediction: ΔT=%.1f°C, slope=%.2f°C/h, dead_time=%.1f min, correction=%.2f, "
|
||||
"duration=%.1f min, confidence=%.2f",
|
||||
temp_delta,
|
||||
learned_slope,
|
||||
dead_time_minutes,
|
||||
correction_factor,
|
||||
anticipation_minutes,
|
||||
confidence,
|
||||
)
|
||||
|
||||
return PredictionResult(
|
||||
anticipated_start_time=anticipated_start,
|
||||
estimated_duration_minutes=anticipation_minutes,
|
||||
confidence_level=confidence,
|
||||
learned_heating_slope=learned_slope,
|
||||
)
|
||||
|
||||
def _calculate_environmental_correction(
|
||||
self,
|
||||
outdoor_temp: float | None,
|
||||
humidity: float | None,
|
||||
cloud_coverage: float | None,
|
||||
) -> float:
|
||||
"""Calculate combined environmental correction factor.
|
||||
|
||||
This method combines multiple environmental factors that affect
|
||||
heating efficiency:
|
||||
- Outdoor temperature (heat loss)
|
||||
- Indoor humidity (thermal mass effect)
|
||||
- Cloud coverage (solar gain)
|
||||
|
||||
Args:
|
||||
outdoor_temp: Outdoor temperature in Celsius
|
||||
humidity: Indoor humidity percentage (0-100)
|
||||
cloud_coverage: Cloud coverage percentage (0-100)
|
||||
|
||||
Returns:
|
||||
Combined correction factor (>1 means slower heating)
|
||||
"""
|
||||
correction_factor = 1.0
|
||||
|
||||
# Outdoor temperature factor: colder outside means slower heating
|
||||
# Formula: outdoor_factor = 1 + (OUTDOOR_TEMP_REFERENCE - outdoor_temp) * OUTDOOR_TEMP_FACTOR
|
||||
# At outdoor_temp = 20°C: factor = 1.0 (no impact)
|
||||
# At outdoor_temp = 0°C: factor = 2.0 (heating takes twice as long)
|
||||
# At outdoor_temp = -10°C: factor = 2.5 (even slower)
|
||||
if outdoor_temp is not None:
|
||||
outdoor_factor = 1.0 + (OUTDOOR_TEMP_REFERENCE - outdoor_temp) * OUTDOOR_TEMP_FACTOR
|
||||
outdoor_factor = max(0.5, outdoor_factor) # Minimum factor of 0.5
|
||||
correction_factor *= outdoor_factor
|
||||
_LOGGER.debug("Outdoor temp %.1f°C -> factor %.2f", outdoor_temp, outdoor_factor)
|
||||
|
||||
# Humidity factor: higher humidity makes heating feel slower
|
||||
# Formula: humidity_factor = 1 + (humidity - HUMIDITY_REFERENCE) * HUMIDITY_FACTOR
|
||||
# At 50% humidity: factor = 1.0 (neutral)
|
||||
# At 80% humidity: factor = 1.06 (6% slower)
|
||||
# At 20% humidity: factor = 0.94 (6% faster)
|
||||
if humidity is not None:
|
||||
humidity_factor = 1.0 + (humidity - HUMIDITY_REFERENCE) * HUMIDITY_FACTOR
|
||||
humidity_factor = max(0.8, min(1.2, humidity_factor))
|
||||
correction_factor *= humidity_factor
|
||||
_LOGGER.debug("Humidity %.1f%% -> factor %.2f", humidity, humidity_factor)
|
||||
|
||||
# Solar gain factor: less cloud coverage means more solar heat gain
|
||||
# Formula: solar_factor = 1 - (100 - cloud_coverage) * CLOUD_COVERAGE_FACTOR
|
||||
# At 100% cloud: factor = 1.0 (no solar gain)
|
||||
# At 0% cloud (clear sky): factor = 0.9 (10% faster due to sun)
|
||||
# At 50% cloud: factor = 0.95 (5% faster)
|
||||
if cloud_coverage is not None:
|
||||
solar_factor = 1.0 - (100.0 - cloud_coverage) * CLOUD_COVERAGE_FACTOR
|
||||
solar_factor = max(0.8, min(1.0, solar_factor))
|
||||
correction_factor *= solar_factor
|
||||
_LOGGER.debug("Cloud coverage %.1f%% -> factor %.2f", cloud_coverage, solar_factor)
|
||||
|
||||
return correction_factor
|
||||
|
||||
def _calculate_confidence(
|
||||
self,
|
||||
learned_slope: float,
|
||||
outdoor_temp: float | None,
|
||||
humidity: float | None,
|
||||
) -> float:
|
||||
"""Calculate confidence level in the prediction.
|
||||
|
||||
Confidence is based on:
|
||||
- Slope validity (higher slope = better learning)
|
||||
- Available environmental data (more data = better prediction)
|
||||
|
||||
Args:
|
||||
learned_slope: Learned heating slope in °C/hour
|
||||
outdoor_temp: Outdoor temperature (if available)
|
||||
humidity: Indoor humidity (if available)
|
||||
|
||||
Returns:
|
||||
Confidence level between 0.0 and 1.0
|
||||
"""
|
||||
# Base confidence from slope validity
|
||||
if learned_slope > HIGH_CONFIDENCE_SLOPE:
|
||||
confidence = BASE_HIGH_CONFIDENCE
|
||||
elif learned_slope > MEDIUM_CONFIDENCE_SLOPE:
|
||||
confidence = BASE_MEDIUM_CONFIDENCE
|
||||
else:
|
||||
confidence = BASE_LOW_CONFIDENCE
|
||||
|
||||
# Adjust confidence based on available environmental data
|
||||
data_availability = 0
|
||||
if outdoor_temp is not None:
|
||||
data_availability += 1
|
||||
if humidity is not None:
|
||||
data_availability += 1
|
||||
|
||||
# Increase confidence slightly with more environmental data
|
||||
confidence += data_availability * CONFIDENCE_BOOST_PER_SENSOR
|
||||
|
||||
# Cap at 1.0
|
||||
return min(1.0, confidence)
|
||||
Reference in New Issue
Block a user