mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
refactor(logging): use details logger for verbose period calculation logs
Moved verbose debug logging to separate _LOGGER_DETAILS logger: - core.py: Outlier flex capping messages - outlier_filtering.py: Spike detection, context validation, smoothing details - period_building.py: Level filter details, gap tolerance info - relaxation.py: Per-phase iteration details, filter combination attempts Pattern: Main _LOGGER for high-level progress, _LOGGER_DETAILS for step-by-step Benefits: - Users can disable verbose logs via logger configuration - Main DEBUG log stays readable (high-level flow) - Details available when needed for troubleshooting Added: - period_overlap.py: Docstring for extend_period_if_adjacent() Impact: Cleaner log output by default. Enable details logger (homeassistant.components.tibber_prices.coordinator.period_handlers.details) for deep debugging.
This commit is contained in:
parent
9ae618fff9
commit
3c69807c05
5 changed files with 37 additions and 29 deletions
|
|
@ -127,9 +127,13 @@ def calculate_periods(
|
|||
# High flex (>25%) makes outlier detection too permissive, accepting
|
||||
# unstable price contexts as "normal". This breaks period formation.
|
||||
# User's flex setting still applies to period criteria (in_flex check).
|
||||
|
||||
# Import details logger locally (core.py imports logger locally in function)
|
||||
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details") # noqa: N806
|
||||
|
||||
outlier_flex = min(abs(flex) * 100, MAX_OUTLIER_FLEX * 100)
|
||||
if abs(flex) * 100 > MAX_OUTLIER_FLEX * 100:
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sOutlier filtering: Using capped flex %.1f%% (user setting: %.1f%%)",
|
||||
INDENT_L0,
|
||||
outlier_flex,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import logging
|
|||
from typing import NamedTuple
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
||||
|
||||
# Outlier filtering constants
|
||||
MIN_CONTEXT_SIZE = 3 # Minimum intervals needed before/after for analysis
|
||||
|
|
@ -52,7 +53,7 @@ def _should_skip_tail_check(
|
|||
) -> bool:
|
||||
"""Return True when remaining intervals fall inside tail window and log why."""
|
||||
if remaining_intervals < tail_window:
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sSpike at %s: Skipping %s check (only %d intervals remaining)",
|
||||
INDENT_L0,
|
||||
interval_label,
|
||||
|
|
@ -190,7 +191,7 @@ def _validate_spike_candidate(
|
|||
|
||||
context_diff_pct = abs(avg_after - avg_before) / avg_before if avg_before > 0 else 0
|
||||
if context_diff_pct > candidate.flexibility_ratio:
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sInterval %s: Context unstable (%.1f%% change) - not a spike",
|
||||
INDENT_L0,
|
||||
candidate.current.get("startsAt", "unknown interval"),
|
||||
|
|
@ -204,7 +205,7 @@ def _validate_spike_candidate(
|
|||
"asymmetry",
|
||||
candidate.current.get("startsAt", "unknown interval"),
|
||||
) and not _check_symmetry(avg_before, avg_after, candidate.stats["std_dev"]):
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sSpike at %s rejected: Asymmetric (before=%.2f, after=%.2f ct/kWh)",
|
||||
INDENT_L0,
|
||||
candidate.current.get("startsAt", "unknown interval"),
|
||||
|
|
@ -222,7 +223,7 @@ def _validate_spike_candidate(
|
|||
return True
|
||||
|
||||
if _detect_zigzag_pattern(candidate.analysis_window, candidate.stats["std_dev"]):
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sSpike at %s rejected: Zigzag/cluster pattern detected",
|
||||
INDENT_L0,
|
||||
candidate.current.get("startsAt", "unknown interval"),
|
||||
|
|
@ -330,7 +331,7 @@ def filter_price_outliers(
|
|||
result.append(smoothed)
|
||||
smoothed_count += 1
|
||||
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sSmoothed spike at %s: %.2f → %.2f ct/kWh (residual: %.2f, tolerance: %.2f, trend_slope: %.4f)",
|
||||
INDENT_L0,
|
||||
current.get("startsAt", f"index {i}"),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from .level_filtering import (
|
|||
from .types import TibberPricesIntervalCriteria
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
||||
|
||||
# Module-local log indentation (each module starts at level 0)
|
||||
INDENT_L0 = "" # Entry point / main function
|
||||
|
|
@ -92,7 +93,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
|||
level_filter_active = True
|
||||
filter_direction = "≥" if reverse_sort else "≤"
|
||||
gap_info = f", gap_tolerance={gap_count}" if gap_count > 0 else ""
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sLevel filter active: %s (order %s, require interval level %s filter level%s)",
|
||||
INDENT_L0,
|
||||
level_filter.upper(),
|
||||
|
|
@ -102,7 +103,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
|||
)
|
||||
else:
|
||||
status = "RELAXED to ANY" if (level_filter and level_filter.lower() == "any") else "DISABLED (not configured)"
|
||||
_LOGGER.debug("%sLevel filter: %s (accepting all levels)", INDENT_L0, status)
|
||||
_LOGGER_DETAILS.debug("%sLevel filter: %s (accepting all levels)", INDENT_L0, status)
|
||||
|
||||
periods: list[list[dict]] = []
|
||||
current_period: list[dict] = []
|
||||
|
|
@ -191,14 +192,14 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
|||
|
||||
# Log detailed filter statistics
|
||||
if intervals_checked > 0:
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sFilter statistics: %d intervals checked",
|
||||
INDENT_L0,
|
||||
intervals_checked,
|
||||
)
|
||||
if intervals_filtered_by_flex > 0:
|
||||
flex_pct = (intervals_filtered_by_flex / intervals_checked) * 100
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s Filtered by FLEX (price too far from ref): %d/%d (%.1f%%)",
|
||||
INDENT_L0,
|
||||
intervals_filtered_by_flex,
|
||||
|
|
@ -207,7 +208,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
|||
)
|
||||
if intervals_filtered_by_min_distance > 0:
|
||||
distance_pct = (intervals_filtered_by_min_distance / intervals_checked) * 100
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s Filtered by MIN_DISTANCE (price too close to avg): %d/%d (%.1f%%)",
|
||||
INDENT_L0,
|
||||
intervals_filtered_by_min_distance,
|
||||
|
|
@ -216,7 +217,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
|||
)
|
||||
if level_filter_active and intervals_filtered_by_level > 0:
|
||||
level_pct = (intervals_filtered_by_level / intervals_checked) * 100
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s Filtered by LEVEL (wrong price level): %d/%d (%.1f%%)",
|
||||
INDENT_L0,
|
||||
intervals_filtered_by_level,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ if TYPE_CHECKING:
|
|||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
||||
|
||||
# Module-local log indentation (each module starts at level 0)
|
||||
INDENT_L0 = "" # Entry point / main function
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from .types import (
|
|||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER_DETAILS = logging.getLogger(__name__ + ".details")
|
||||
|
||||
# Flex thresholds for warnings (see docs/development/period-calculation-theory.md)
|
||||
# With relaxation active, high base flex is counterproductive (reduces relaxation effectiveness)
|
||||
|
|
@ -204,38 +205,38 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
|||
|
||||
# Detailed DEBUG-level context header
|
||||
period_type_full = "PEAK PRICE (most expensive)" if config.reverse_sort else "BEST PRICE (cheapest)"
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s========== %s PERIODS ==========",
|
||||
INDENT_L0,
|
||||
period_type_full,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sRelaxation: %s",
|
||||
INDENT_L0,
|
||||
"ENABLED (user setting: ON)" if enable_relaxation else "DISABLED by user configuration",
|
||||
)
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sBase config: flex=%.1f%%, min_length=%d min",
|
||||
INDENT_L0,
|
||||
abs(config.flex) * 100,
|
||||
config.min_period_length,
|
||||
)
|
||||
if enable_relaxation:
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sRelaxation target: %d periods per day",
|
||||
INDENT_L0,
|
||||
min_periods,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sRelaxation strategy: 3%% fixed flex increment per step (%d flex levels x 2 filter combinations)",
|
||||
INDENT_L0,
|
||||
max_relaxation_attempts,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sEarly exit: After EACH filter combination when target reached",
|
||||
INDENT_L0,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s=============================================",
|
||||
INDENT_L0,
|
||||
)
|
||||
|
|
@ -266,7 +267,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
|||
"Calculating baseline periods for %d days...",
|
||||
total_days,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sProcessing ALL %d price intervals together (yesterday+today+tomorrow, allows midnight crossing)",
|
||||
INDENT_L1,
|
||||
len(all_prices),
|
||||
|
|
@ -285,7 +286,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
|||
day_periods = periods_by_day.get(day, [])
|
||||
period_count = len(day_periods)
|
||||
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sDay %s baseline: Found %d periods%s",
|
||||
INDENT_L1,
|
||||
day,
|
||||
|
|
@ -302,7 +303,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
|||
|
||||
if enable_relaxation and days_meeting_requirement < total_days:
|
||||
# At least one day doesn't have enough periods
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sBaseline insufficient (%d/%d days met target) - starting relaxation",
|
||||
INDENT_L1,
|
||||
days_meeting_requirement,
|
||||
|
|
@ -334,7 +335,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
|||
if period_count >= min_periods:
|
||||
days_meeting_requirement += 1
|
||||
elif enable_relaxation:
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%sAll %d days met target with baseline - no relaxation needed",
|
||||
INDENT_L1,
|
||||
total_days,
|
||||
|
|
@ -422,7 +423,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
|||
|
||||
# Stop if we exceed hard maximum
|
||||
if current_flex > MAX_FLEX_HARD_LIMIT:
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s Reached 50%% flex hard limit",
|
||||
INDENT_L2,
|
||||
)
|
||||
|
|
@ -436,7 +437,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
|||
|
||||
# Try current flex with level="any" (in relaxation mode)
|
||||
if original_level_filter != "any":
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s Flex=%.1f%%: OVERRIDING level_filter: %s → ANY",
|
||||
INDENT_L2,
|
||||
current_flex * 100,
|
||||
|
|
@ -450,7 +451,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
|||
)
|
||||
|
||||
phase_label_full = f"flex={current_flex * 100:.1f}% +level_any"
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s Trying %s: config has %d intervals (all days together), level_filter=%s",
|
||||
INDENT_L2,
|
||||
phase_label_full,
|
||||
|
|
@ -462,7 +463,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
|||
result = calculate_periods(all_prices, config=relaxed_config, time=time)
|
||||
new_periods = result["periods"]
|
||||
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s %s: calculate_periods returned %d periods",
|
||||
INDENT_L2,
|
||||
phase_label_full,
|
||||
|
|
@ -494,7 +495,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
|||
if period_count >= min_periods:
|
||||
days_meeting_requirement += 1
|
||||
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s Day %s: %d periods%s",
|
||||
INDENT_L2,
|
||||
day,
|
||||
|
|
@ -503,7 +504,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
|||
)
|
||||
|
||||
total_periods = len(combined)
|
||||
_LOGGER.debug(
|
||||
_LOGGER_DETAILS.debug(
|
||||
"%s %s: found %d periods total, %d/%d days meet requirement",
|
||||
INDENT_L2,
|
||||
phase_label_full,
|
||||
|
|
|
|||
Loading…
Reference in a new issue