186 lines
6.5 KiB
Python
186 lines
6.5 KiB
Python
"""Heating Orchestrator.
|
|
|
|
The orchestrator composes use cases to implement complex workflows.
|
|
It coordinates multiple use cases but contains NO business logic itself.
|
|
|
|
This is the main entry point for heating operations from the infrastructure layer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .use_cases import (
|
|
CalculateAnticipationUseCase,
|
|
CheckOvershootRiskUseCase,
|
|
ControlPreheatingUseCase,
|
|
ScheduleAnticipationActionUseCase,
|
|
UpdateCacheDataUseCase,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class HeatingOrchestrator:
|
|
"""Orchestrates heating use cases.
|
|
|
|
The orchestrator is responsible for:
|
|
1. Composing use cases to implement workflows
|
|
2. Coordinating the execution order of use cases
|
|
3. Managing cross-use-case dependencies
|
|
|
|
NO business logic - pure composition and coordination.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
calculate_anticipation: CalculateAnticipationUseCase,
|
|
control_preheating: ControlPreheatingUseCase,
|
|
schedule_anticipation_action: ScheduleAnticipationActionUseCase,
|
|
check_overshoot_risk: CheckOvershootRiskUseCase,
|
|
update_cache: UpdateCacheDataUseCase,
|
|
) -> None:
|
|
"""Initialize the orchestrator with use cases.
|
|
|
|
Args:
|
|
calculate_anticipation: Use case for calculating anticipation data
|
|
control_preheating: Use case for controlling preheating
|
|
schedule_anticipation_action: Use case for scheduling anticipation actions
|
|
check_overshoot_risk: Use case for checking overshoot risk
|
|
update_cache: Use case for updating cache
|
|
"""
|
|
_LOGGER.debug("Initializing HeatingOrchestrator")
|
|
self._calculate_anticipation = calculate_anticipation
|
|
self._control_preheating = control_preheating
|
|
self._schedule_anticipation_action = schedule_anticipation_action
|
|
self._check_overshoot_risk = check_overshoot_risk
|
|
self._update_cache = update_cache
|
|
|
|
async def calculate_anticipation_only(
|
|
self,
|
|
target_time: datetime | None = None,
|
|
target_temp: float | None = None,
|
|
) -> dict:
|
|
"""Calculate anticipation data only (no scheduling).
|
|
|
|
For users who use the service without a scheduler (via REST API).
|
|
|
|
Args:
|
|
target_time: Target time for heating
|
|
target_temp: Target temperature
|
|
|
|
Returns:
|
|
Dict with anticipation data
|
|
"""
|
|
_LOGGER.debug("Entering HeatingOrchestrator.calculate_anticipation_only()")
|
|
|
|
result = await self._calculate_anticipation.calculate_anticipation_datas(
|
|
target_time=target_time,
|
|
target_temp=target_temp,
|
|
)
|
|
|
|
_LOGGER.debug("Exiting calculate_anticipation_only() -> data")
|
|
return result
|
|
|
|
async def disable_preheating(self, scheduler_entity_id: str) -> None:
|
|
"""Disable preheating (cancel timer and active preheating).
|
|
|
|
Args:
|
|
scheduler_entity_id: Scheduler entity to cancel
|
|
"""
|
|
_LOGGER.debug("Entering HeatingOrchestrator.disable_preheating()")
|
|
|
|
# Delegate to schedule_anticipation_action to cancel timer
|
|
await self._schedule_anticipation_action.cancel_action()
|
|
|
|
# Cancel active preheating if any
|
|
await self._control_preheating.cancel_preheating(scheduler_entity_id)
|
|
|
|
_LOGGER.info("Preheating disabled")
|
|
_LOGGER.debug("Exiting HeatingOrchestrator.disable_preheating()")
|
|
|
|
async def reset_all_learning_data(self, device_id: str) -> None:
|
|
"""Reset all learned data (LHS + cycles).
|
|
|
|
Args:
|
|
device_id: Device identifier
|
|
"""
|
|
_LOGGER.debug("Entering HeatingOrchestrator.reset_all_learning_data()")
|
|
|
|
await self._update_cache.reset_cache(device_id)
|
|
|
|
_LOGGER.debug("Exiting HeatingOrchestrator.reset_all_learning_data()")
|
|
|
|
def is_preheating_active(self) -> bool:
|
|
"""Check if preheating is currently active.
|
|
|
|
Returns:
|
|
True if preheating is active, False otherwise
|
|
"""
|
|
return self._control_preheating.is_preheating_active()
|
|
|
|
async def calculate_and_schedule_anticipation(self, ihp_enabled: bool = True) -> dict[str, Any]:
|
|
"""Calculate anticipation and handle scheduling based on IHP status.
|
|
|
|
This orchestrator method:
|
|
1. Calculates anticipation data
|
|
2. Delegates scheduling decisions to ScheduleAnticipationActionUseCase
|
|
3. Always returns anticipation data structure
|
|
|
|
Args:
|
|
ihp_enabled: Whether IHP preheating is enabled
|
|
|
|
Returns:
|
|
Dict with anticipation data for sensors (always returns structure)
|
|
"""
|
|
_LOGGER.debug(
|
|
"Entering HeatingOrchestrator.calculate_and_schedule_anticipation(ihp_enabled=%s)",
|
|
ihp_enabled,
|
|
)
|
|
|
|
# Step 1: Always calculate anticipation data regardless of IHP state.
|
|
# The switch only controls whether preheating is scheduled, not whether
|
|
# calculations happen. This ensures sensors and LHS caches stay up-to-date.
|
|
anticipation_data = await self._calculate_anticipation.calculate_anticipation_datas()
|
|
|
|
# Step 2: Delegate scheduling decisions to use case (respects ihp_enabled)
|
|
await self._schedule_anticipation_action.handle_anticipation_scheduling(
|
|
anticipation_data,
|
|
ihp_enabled,
|
|
)
|
|
|
|
_LOGGER.debug("Exiting HeatingOrchestrator.calculate_and_schedule_anticipation() -> data")
|
|
return anticipation_data
|
|
|
|
async def check_and_prevent_overshoot(self, scheduler_entity_id: str) -> bool:
|
|
"""Check overshoot risk and cancel preheating if needed.
|
|
|
|
Args:
|
|
scheduler_entity_id: Scheduler entity to cancel if overshoot detected
|
|
|
|
Returns:
|
|
True if overshoot detected and cancellation triggered, False otherwise
|
|
"""
|
|
_LOGGER.debug(
|
|
"Entering HeatingOrchestrator.check_and_prevent_overshoot(scheduler=%s)",
|
|
scheduler_entity_id,
|
|
)
|
|
|
|
overshoot_detected = await self._check_overshoot_risk.check_and_prevent_overshoot(
|
|
scheduler_entity_id=scheduler_entity_id
|
|
)
|
|
|
|
if overshoot_detected:
|
|
await self._schedule_anticipation_action.cancel_action()
|
|
|
|
_LOGGER.debug(
|
|
"Exiting HeatingOrchestrator.check_and_prevent_overshoot() -> %s",
|
|
overshoot_detected,
|
|
)
|
|
return overshoot_detected
|