New apps Added

This commit is contained in:
2026-07-08 10:43:39 -04:00
parent 3b1f4bbd75
commit fefc2c8b5c
1114 changed files with 406637 additions and 154 deletions
@@ -0,0 +1,169 @@
"""Heating decision value object."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
class HeatingAction(Enum):
"""Types of heating actions that can be taken."""
START_HEATING = "start_heating"
STOP_HEATING = "stop_heating"
SET_TEMPERATURE = "set_temperature"
NO_ACTION = "no_action"
@dataclass(frozen=True)
class HeatingDecision:
"""Represents a decision about heating control.
This value object encapsulates what action should be taken and why.
Attributes:
action: The type of action to take
target_temp: Target temperature if starting heating (None otherwise)
reason: Human-readable explanation for the decision
"""
action: HeatingAction
target_temp: float | None = None
reason: str = ""
def __post_init__(self) -> None:
"""Validate the heating decision data."""
if self.action == HeatingAction.START_HEATING and self.target_temp is None:
raise ValueError("START_HEATING action requires a target temperature")
if self.action == HeatingAction.SET_TEMPERATURE and self.target_temp is None:
raise ValueError("SET_TEMPERATURE action requires a target temperature")
@dataclass(frozen=True)
class TariffPeriodDetail:
"""Represents energy consumption and cost details for a specific tariff period."""
tariff_price_eur_per_kwh: float
energy_kwh: float
heating_duration_minutes: float
cost_euro: float
@dataclass(frozen=True)
class HeatingCycle:
"""Represents a single heating cycle, encapsulating all its relevant data.
This value object provides a complete and immutable snapshot of a heating period,
including its duration, temperature changes, and energy consumption details.
Attributes:
start_time: The exact datetime when the heating cycle started.
end_time: The exact datetime when the heating cycle ended.
target_temp: The target temperature set for this heating cycle.
end_temp: The actual temperature reached at the end of the heating cycle.
start_temp: The temperature at the beginning of the heating cycle.
tariff_details: A list of TariffDetail objects, breaking down energy, duration,
and cost by specific TariffPeriodDetail periods within the cycle.
dead_time_cycle_minutes: Dead time for this specific cycle in minutes. Time from
cycle start to first measurable temperature change.
None if cannot be determined.
min_effective_duration_minutes: Minimum effective heating duration (in minutes) required
to compute a valid slope. Effective duration is
total_duration dead_time. Cycles whose effective window
is shorter than this threshold return 0.0 for
``avg_heating_slope`` to prevent aberrant values caused by
near-zero denominators. Defaults to 5.0 minutes.
"""
device_id: str
start_time: datetime
end_time: datetime
target_temp: float
end_temp: float
start_temp: float
tariff_details: list[TariffPeriodDetail] | None = None
dead_time_cycle_minutes: float | None = None
min_effective_duration_minutes: float = 5.0
@property
def avg_heating_slope(self) -> float:
"""Calculates the average heating slope in °C/hour for the heating cycle.
Excludes the dead_time_cycle period to get the true heating slope once
the system is actively heating (without initial inertia).
Returns 0.0 when the effective heating duration (after subtracting dead_time) is shorter
than ``min_effective_duration_minutes``. This guards against aberrant slope values that
arise when dead_time ≈ total_duration, leaving an effective duration of only a few
microseconds and producing slopes in the range of 100 000200 000 °C/h.
"""
# Calculate effective start time (after dead_time_cycle)
if self.dead_time_cycle_minutes and self.dead_time_cycle_minutes > 0:
effective_start_time = self.start_time + timedelta(minutes=self.dead_time_cycle_minutes)
duration_hours = (self.end_time - effective_start_time).total_seconds() / 3600
else:
duration_hours = (self.end_time - self.start_time).total_seconds() / 3600
if duration_hours <= 0:
return 0.0
# Guard: reject cycles whose effective heating window is too narrow.
# When dead_time ≈ total_duration the slope formula amplifies noise by orders of magnitude.
effective_duration_minutes = duration_hours * 60.0
if effective_duration_minutes < self.min_effective_duration_minutes:
return 0.0
temp_increase = self.end_temp - self.start_temp
return temp_increase / duration_hours
@property
def duration_minutes(self) -> float:
"""Calculates the total duration of the heating cycle in minutes."""
return (self.end_time - self.start_time).total_seconds() / 60
@property
def temp_delta(self) -> float:
"""Calculates the difference between the target temperature and the end temperature."""
return self.target_temp - self.end_temp
@property
def start_hour(self) -> int:
"""Returns the hour (0-23) when the heating cycle started."""
return self.start_time.hour
@property
def end_hour(self) -> int:
"""Returns the hour (0-23) when the heating cycle ended."""
return self.end_time.hour
@property
def start_weekday(self) -> int:
"""Returns the weekday (0=Monday, 6=Sunday) when the heating cycle started."""
return self.start_time.weekday()
@property
def end_weekday(self) -> int:
"""Returns the weekday (0=Monday, 6=Sunday) when the heating cycle ended."""
return self.end_time.weekday()
@property
def total_energy_kwh(self) -> float:
"""Calculates the total energy consumed during the cycle in kWh from tariff details."""
return sum(detail.energy_kwh for detail in (self.tariff_details or []))
@property
def total_heating_duration_minutes(self) -> float:
"""Calculates the total heating duration in minutes from tariff details."""
return sum(detail.heating_duration_minutes for detail in (self.tariff_details or []))
@property
def total_cost_euro(self) -> float:
"""Calculates the total cost in euros from tariff details."""
return sum(detail.cost_euro for detail in (self.tariff_details or []))
def __post_init__(self) -> None:
"""Validate the heating cycle data."""
if self.start_time >= self.end_time:
raise ValueError("Start time must be before end time for a heating cycle.")