From 8adefc7a7e289dc1d4fb46d187b3fa48d68bc547 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:50:24 +0000 Subject: [PATCH] chore(docs): format Python code blocks in markdown files with ruff Run `ruff format .` to reformat Python code blocks inside markdown files (AGENTS.md and docs/**/*.md). Ruff 0.16.0 formats embedded Python code blocks in .md files, and the CI `lint-check` step runs `ruff format . --check` which was failing on 109 files. Release-Notes: skip User-Impact: none Agent-Logs-Url: https://github.com/jpawlowski/hass.tibber_prices/sessions/05437696-813b-432a-b05e-af698a9fa903 Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com> --- AGENTS.md | 100 +++++++++--------- docs/developer/docs/architecture.md | 18 ++-- docs/developer/docs/caching-strategy.md | 8 +- docs/developer/docs/coding-guidelines.md | 2 + docs/developer/docs/critical-patterns.md | 2 +- docs/developer/docs/debugging.md | 12 +-- docs/developer/docs/performance.md | 22 ++-- .../docs/period-calculation-theory.md | 4 +- docs/developer/docs/repairs-system.md | 8 +- docs/developer/docs/timer-architecture.md | 12 +-- .../version-v0.21.0/architecture.md | 18 ++-- .../version-v0.21.0/caching-strategy.md | 8 +- .../version-v0.21.0/coding-guidelines.md | 2 + .../version-v0.21.0/critical-patterns.md | 2 +- .../version-v0.21.0/debugging.md | 12 +-- .../version-v0.21.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.21.0/repairs-system.md | 8 +- .../version-v0.21.0/timer-architecture.md | 12 +-- .../version-v0.22.0/architecture.md | 18 ++-- .../version-v0.22.0/caching-strategy.md | 8 +- .../version-v0.22.0/coding-guidelines.md | 2 + .../version-v0.22.0/critical-patterns.md | 2 +- .../version-v0.22.0/debugging.md | 12 +-- .../version-v0.22.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.22.0/repairs-system.md | 8 +- .../version-v0.22.0/timer-architecture.md | 12 +-- .../version-v0.22.1/architecture.md | 18 ++-- .../version-v0.22.1/caching-strategy.md | 8 +- .../version-v0.22.1/coding-guidelines.md | 2 + .../version-v0.22.1/critical-patterns.md | 2 +- .../version-v0.22.1/debugging.md | 12 +-- .../version-v0.22.1/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.22.1/repairs-system.md | 8 +- .../version-v0.22.1/timer-architecture.md | 12 +-- .../version-v0.23.0/architecture.md | 18 ++-- .../version-v0.23.0/caching-strategy.md | 8 +- .../version-v0.23.0/coding-guidelines.md | 2 + .../version-v0.23.0/critical-patterns.md | 2 +- .../version-v0.23.0/debugging.md | 12 +-- .../version-v0.23.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.23.0/repairs-system.md | 8 +- .../version-v0.23.0/timer-architecture.md | 12 +-- .../version-v0.23.1/architecture.md | 18 ++-- .../version-v0.23.1/caching-strategy.md | 8 +- .../version-v0.23.1/coding-guidelines.md | 2 + .../version-v0.23.1/critical-patterns.md | 2 +- .../version-v0.23.1/debugging.md | 12 +-- .../version-v0.23.1/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.23.1/repairs-system.md | 8 +- .../version-v0.23.1/timer-architecture.md | 12 +-- .../version-v0.24.0/architecture.md | 18 ++-- .../version-v0.24.0/caching-strategy.md | 8 +- .../version-v0.24.0/coding-guidelines.md | 2 + .../version-v0.24.0/critical-patterns.md | 2 +- .../version-v0.24.0/debugging.md | 12 +-- .../version-v0.24.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.24.0/repairs-system.md | 8 +- .../version-v0.24.0/timer-architecture.md | 12 +-- .../version-v0.27.0/architecture.md | 18 ++-- .../version-v0.27.0/caching-strategy.md | 8 +- .../version-v0.27.0/coding-guidelines.md | 2 + .../version-v0.27.0/critical-patterns.md | 2 +- .../version-v0.27.0/debugging.md | 12 +-- .../version-v0.27.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.27.0/repairs-system.md | 8 +- .../version-v0.27.0/timer-architecture.md | 12 +-- .../version-v0.28.0/architecture.md | 18 ++-- .../version-v0.28.0/caching-strategy.md | 8 +- .../version-v0.28.0/coding-guidelines.md | 2 + .../version-v0.28.0/critical-patterns.md | 2 +- .../version-v0.28.0/debugging.md | 12 +-- .../version-v0.28.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.28.0/repairs-system.md | 8 +- .../version-v0.28.0/timer-architecture.md | 12 +-- .../version-v0.29.0/architecture.md | 18 ++-- .../version-v0.29.0/caching-strategy.md | 8 +- .../version-v0.29.0/coding-guidelines.md | 2 + .../version-v0.29.0/critical-patterns.md | 2 +- .../version-v0.29.0/debugging.md | 12 +-- .../version-v0.29.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.29.0/repairs-system.md | 8 +- .../version-v0.29.0/timer-architecture.md | 12 +-- .../version-v0.30.0/architecture.md | 18 ++-- .../version-v0.30.0/caching-strategy.md | 8 +- .../version-v0.30.0/coding-guidelines.md | 2 + .../version-v0.30.0/critical-patterns.md | 2 +- .../version-v0.30.0/debugging.md | 12 +-- .../version-v0.30.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.30.0/repairs-system.md | 8 +- .../version-v0.30.0/timer-architecture.md | 12 +-- .../version-v0.31.0/architecture.md | 18 ++-- .../version-v0.31.0/caching-strategy.md | 8 +- .../version-v0.31.0/coding-guidelines.md | 2 + .../version-v0.31.0/critical-patterns.md | 2 +- .../version-v0.31.0/debugging.md | 12 +-- .../version-v0.31.0/performance.md | 22 ++-- .../period-calculation-theory.md | 4 +- .../version-v0.31.0/repairs-system.md | 8 +- .../version-v0.31.0/timer-architecture.md | 12 +-- 109 files changed, 543 insertions(+), 613 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a31a3af..ba7d7a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -702,10 +702,10 @@ Example: daily_min=10 ct, daily_avg=20 ct, flex=50%, min_distance=5% **Key Constants** (defined in `coordinator/period_handlers/core.py`): ```python -MAX_SAFE_FLEX = 0.50 # 50% absolute maximum -MAX_OUTLIER_FLEX = 0.25 # 25% for stable outlier detection +MAX_SAFE_FLEX = 0.50 # 50% absolute maximum +MAX_OUTLIER_FLEX = 0.25 # 25% for stable outlier detection FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # INFO warning at 25% base flex -FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # WARNING at 30% base flex +FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # WARNING at 30% base flex ``` **Relaxation Strategy** (`coordinator/period_handlers/relaxation.py`): @@ -725,10 +725,10 @@ FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # WARNING at 30% base flex **Default Configuration Values** (`const.py`): ```python -DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode -DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) -DEFAULT_RELAXATION_ATTEMPTS_BEST = 11 # 11 steps: 15% → 48% (3% increment per step) -DEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # 11 steps: 20% → 50% (3% increment per step) +DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode +DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) +DEFAULT_RELAXATION_ATTEMPTS_BEST = 11 # 11 steps: 15% → 48% (3% increment per step) +DEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # 11 steps: 20% → 50% (3% increment per step) ``` The relaxation increment is **hard-coded at 3% per step** in `relaxation.py` for reliability and predictability. This prevents configuration issues with high base flex values while still allowing sufficient escalation to the 50% hard maximum. @@ -1297,12 +1297,14 @@ If you see errors from one tool, understand which tool should fix them: def process_data(time: TimeService | None) -> None: result = time.now() # Error: 'None' has no attribute 'now' + # ✅ GOOD - Guard against None def process_data(time: TimeService | None) -> None: if time is None: return result = time.now() # OK - type narrowed to TimeService + # ✅ BETTER - Make it required if always provided def process_data(time: TimeService) -> None: result = time.now() # OK - never None @@ -1319,6 +1321,7 @@ if TYPE_CHECKING: # Custom callback type that accepts TimeService TimeServiceCallback = Callable[[TimeService], None] + # Use in class definition class ListenerManager: def __init__(self) -> None: @@ -1360,10 +1363,12 @@ if (interval_time := time.get_interval_time(price)) is not None: def get_timestamp() -> str: return datetime.now() # Error: incompatible return type + # ✅ GOOD - Match return type to actual return value def get_timestamp() -> datetime: return datetime.now() # OK + # ✅ ALSO GOOD - Convert if signature requires string def get_timestamp() -> str: return datetime.now().isoformat() # OK - returns str @@ -1623,8 +1628,10 @@ If you create a helper class that is ONLY used within a single module file: # ✅ CORRECT - Private class with underscore prefix class _InternalHelper: """Helper class used only within this module.""" + pass + # Usage: Only in the same file, never imported elsewhere result = _InternalHelper().process() ``` @@ -1641,6 +1648,7 @@ result = _InternalHelper().process() # In coordinator/price_data_manager.py class _ApiRetryStateMachine: """Internal state machine for retry logic. Never used outside this file.""" + def __init__(self, max_retries: int) -> None: self._attempts = 0 self._max_retries = max_retries @@ -1733,12 +1741,12 @@ Use consistent indentation to show logical structure (DEBUG level only): ```python # Define indentation constants at module level -INDENT_L0 = "" # Top-level (0 spaces) -INDENT_L1 = " " # Level 1 (2 spaces) -INDENT_L2 = " " # Level 2 (4 spaces) -INDENT_L3 = " " # Level 3 (6 spaces) +INDENT_L0 = "" # Top-level (0 spaces) +INDENT_L1 = " " # Level 1 (2 spaces) +INDENT_L2 = " " # Level 2 (4 spaces) +INDENT_L3 = " " # Level 3 (6 spaces) INDENT_L4 = " " # Level 4 (8 spaces) -INDENT_L5 = " "# Level 5 (10 spaces) +INDENT_L5 = " " # Level 5 (10 spaces) # Usage example showing logic hierarchy _LOGGER.debug("%sCalculating periods for day %s", INDENT_L0, day_date) @@ -1769,9 +1777,12 @@ _LOGGER.debug( "%s Base flex: %.1f%%\n" "%s Strategy: 4 flex levels × 4 filter combinations", INDENT_L0, - INDENT_L0, "ENABLED" if enable_relaxation else "DISABLED", - INDENT_L0, min_periods, - INDENT_L0, base_flex, + INDENT_L0, + "ENABLED" if enable_relaxation else "DISABLED", + INDENT_L0, + min_periods, + INDENT_L0, + base_flex, INDENT_L0, ) ``` @@ -1955,7 +1966,7 @@ message = "Hello world" html = '
content
' # ❌ Inconsistent quote usage -name = 'tibber_prices' # Ruff will change to double quotes +name = "tibber_prices" # Ruff will change to double quotes ``` **Trailing Commas:** @@ -1968,6 +1979,7 @@ SENSOR_TYPES = [ "max_price", # ← Trailing comma ] + # ✅ Also for function arguments def calculate_average( prices: list[dict], @@ -1976,11 +1988,12 @@ def calculate_average( ) -> float: pass + # ❌ Missing trailing comma SENSOR_TYPES = [ "current_interval_price", "min_price", - "max_price" # Ruff will add trailing comma + "max_price", # Ruff will add trailing comma ] ``` @@ -1992,6 +2005,7 @@ def get_price() -> float: """Return current electricity price.""" return 0.25 + # ✅ Multi-line docstrings: summary line, blank, details def calculate_average(prices: list[dict]) -> float: """Calculate average price from interval list. @@ -2004,9 +2018,10 @@ def calculate_average(prices: list[dict]) -> float: """ return sum(p["total"] for p in prices) / len(prices) + # ❌ Single quotes or missing docstrings on public functions def get_price() -> float: - '''Return price''' # Ruff will change to double quotes + """Return price""" # Ruff will change to double quotes ``` **Line Breaking:** @@ -2020,20 +2035,11 @@ result = some_function( ) # ✅ Break long conditions -if ( - price > threshold - and time_of_day == "peak" - and day_of_week in ["Monday", "Friday"] -): +if price > threshold and time_of_day == "peak" and day_of_week in ["Monday", "Friday"]: do_something() # ✅ Chain methods with line breaks -df = ( - data_frame - .filter(lambda x: x > 0) - .sort_values() - .reset_index() -) +df = data_frame.filter(lambda x: x > 0).sort_values().reset_index() ``` **Type Annotations:** @@ -2044,15 +2050,20 @@ def get_current_interval_price(coordinator: DataUpdateCoordinator) -> float: """Get current price from coordinator.""" return coordinator.data["priceInfo"][0]["total"] + # ✅ Use modern type syntax (Python 3.13) def process_prices(prices: list[dict[str, Any]]) -> dict[str, float]: pass + # ❌ Avoid old-style typing (List, Dict from typing module) from typing import List, Dict + + def process_prices(prices: List[Dict[str, Any]]) -> Dict[str, float]: # Use list, dict instead pass + # ✅ Optional parameters def fetch_data(home_id: str, max_retries: int = 3) -> dict | None: pass @@ -2084,11 +2095,7 @@ prices = [interval["total"] for interval in data] price_map = {interval["startsAt"]: interval["total"] for interval in data} # ✅ Break long comprehensions -prices = [ - interval["total"] - for interval in data - if interval["total"] is not None -] +prices = [interval["total"] for interval in data if interval["total"] is not None] # ❌ Don't use comprehensions for complex logic result = [ # Use regular loop instead @@ -2128,40 +2135,34 @@ Attributes should follow a **logical priority order** to make the most important ```python attributes = { # 1. Time information (when does this apply?) - "timestamp": ..., # ALWAYS FIRST: Reference time for state/attributes validity + "timestamp": ..., # ALWAYS FIRST: Reference time for state/attributes validity "start": ..., "end": ..., "duration_minutes": ..., - # 2. Core decision attributes (what should I do?) - "level": ..., # Price level (VERY_CHEAP, CHEAP, NORMAL, etc.) - "rating_level": ..., # Price rating (LOW, NORMAL, HIGH) - + "level": ..., # Price level (VERY_CHEAP, CHEAP, NORMAL, etc.) + "rating_level": ..., # Price rating (LOW, NORMAL, HIGH) # 3. Price statistics (how much does it cost?) "price_mean": ..., "price_median": ..., "price_min": ..., "price_max": ..., - # 4. Price differences (optional - how does it compare?) "price_diff_from_daily_min": ..., "price_diff_from_daily_min_%": ..., - # 5. Detail information (additional context) "hour": ..., "minute": ..., "time": ..., "period_position": ..., "interval_count": ..., - # 6. Meta information (technical details) - "pricePeriods": [...], # Nested structures last + "pricePeriods": [...], # Nested structures last "priceInfo": [...], - # 7. Extended descriptions (always last) - "description": "...", # Short description from custom_translations (always shown) - "long_description": "...", # Detailed explanation from custom_translations (shown when CONF_EXTENDED_DESCRIPTIONS enabled) - "usage_tips": "...", # Usage examples from custom_translations (shown when CONF_EXTENDED_DESCRIPTIONS enabled) + "description": "...", # Short description from custom_translations (always shown) + "long_description": "...", # Detailed explanation from custom_translations (shown when CONF_EXTENDED_DESCRIPTIONS enabled) + "usage_tips": "...", # Usage examples from custom_translations (shown when CONF_EXTENDED_DESCRIPTIONS enabled) } ``` @@ -2229,6 +2230,7 @@ def _get_sensor_attributes(self) -> dict | None: # Direct implementation returns dict return build_sensor_attributes(...) + # binary_sensor/core.py def _get_sensor_attributes(self) -> dict | None: """Get sensor-specific attributes.""" @@ -2346,7 +2348,7 @@ This ensures timestamp is always the first key in the attribute dict, regardless "price_mean": 18.5, "price_median": 18.3, "interval_count": 4, - "intervals": [...] + "intervals": [...], } # ❌ Bad: Random order makes it hard to scan @@ -2356,7 +2358,7 @@ This ensures timestamp is always the first key in the attribute dict, regardless "rating_level": "LOW", "start": "2025-11-08T14:00:00+01:00", "price_mean": 18.5, - "end": "2025-11-08T15:00:00+01:00" + "end": "2025-11-08T15:00:00+01:00", } ``` diff --git a/docs/developer/docs/architecture.md b/docs/developer/docs/architecture.md index 7046c88..1d3c502 100644 --- a/docs/developer/docs/architecture.md +++ b/docs/developer/docs/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/docs/caching-strategy.md b/docs/developer/docs/caching-strategy.md index ce342a3..c615f28 100644 --- a/docs/developer/docs/caching-strategy.md +++ b/docs/developer/docs/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/docs/coding-guidelines.md b/docs/developer/docs/coding-guidelines.md index a336576..36a9ff9 100644 --- a/docs/developer/docs/coding-guidelines.md +++ b/docs/developer/docs/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/docs/critical-patterns.md b/docs/developer/docs/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/docs/critical-patterns.md +++ b/docs/developer/docs/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/docs/debugging.md b/docs/developer/docs/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/docs/debugging.md +++ b/docs/developer/docs/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/docs/performance.md b/docs/developer/docs/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/docs/performance.md +++ b/docs/developer/docs/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/docs/period-calculation-theory.md b/docs/developer/docs/period-calculation-theory.md index 642f74d..ade2a34 100644 --- a/docs/developer/docs/period-calculation-theory.md +++ b/docs/developer/docs/period-calculation-theory.md @@ -1336,8 +1336,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/docs/repairs-system.md b/docs/developer/docs/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/docs/repairs-system.md +++ b/docs/developer/docs/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/docs/timer-architecture.md b/docs/developer/docs/timer-architecture.md index 606e10f..b576c05 100644 --- a/docs/developer/docs/timer-architecture.md +++ b/docs/developer/docs/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.21.0/architecture.md b/docs/developer/versioned_docs/version-v0.21.0/architecture.md index 7046c88..1d3c502 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.21.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.21.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.21.0/caching-strategy.md index ce342a3..c615f28 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.21.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.21.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.21.0/coding-guidelines.md index a336576..36a9ff9 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.21.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.21.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.21.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.21.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.21.0/debugging.md b/docs/developer/versioned_docs/version-v0.21.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.21.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.21.0/performance.md b/docs/developer/versioned_docs/version-v0.21.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.21.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.21.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.21.0/period-calculation-theory.md index c381ab9..6d20a92 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.21.0/period-calculation-theory.md @@ -1092,8 +1092,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.21.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.21.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.21.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md index 606e10f..b576c05 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.22.0/architecture.md b/docs/developer/versioned_docs/version-v0.22.0/architecture.md index 7046c88..1d3c502 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.22.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.22.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.22.0/caching-strategy.md index ce342a3..c615f28 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.22.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.22.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.22.0/coding-guidelines.md index a336576..36a9ff9 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.22.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.22.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.22.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.22.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.22.0/debugging.md b/docs/developer/versioned_docs/version-v0.22.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.22.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.22.0/performance.md b/docs/developer/versioned_docs/version-v0.22.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.22.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.22.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.22.0/period-calculation-theory.md index c381ab9..6d20a92 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.22.0/period-calculation-theory.md @@ -1092,8 +1092,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.22.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.22.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.22.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.22.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.22.0/timer-architecture.md index 606e10f..b576c05 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.22.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.22.1/architecture.md b/docs/developer/versioned_docs/version-v0.22.1/architecture.md index 7046c88..1d3c502 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/architecture.md +++ b/docs/developer/versioned_docs/version-v0.22.1/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.22.1/caching-strategy.md b/docs/developer/versioned_docs/version-v0.22.1/caching-strategy.md index ce342a3..c615f28 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.22.1/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.22.1/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.22.1/coding-guidelines.md index a336576..36a9ff9 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.22.1/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.22.1/critical-patterns.md b/docs/developer/versioned_docs/version-v0.22.1/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.22.1/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.22.1/debugging.md b/docs/developer/versioned_docs/version-v0.22.1/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/debugging.md +++ b/docs/developer/versioned_docs/version-v0.22.1/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.22.1/performance.md b/docs/developer/versioned_docs/version-v0.22.1/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/performance.md +++ b/docs/developer/versioned_docs/version-v0.22.1/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.22.1/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.22.1/period-calculation-theory.md index db45cb1..b5f8a1b 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.22.1/period-calculation-theory.md @@ -1092,8 +1092,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.22.1/repairs-system.md b/docs/developer/versioned_docs/version-v0.22.1/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.22.1/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.22.1/timer-architecture.md b/docs/developer/versioned_docs/version-v0.22.1/timer-architecture.md index 606e10f..b576c05 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.22.1/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.23.0/architecture.md b/docs/developer/versioned_docs/version-v0.23.0/architecture.md index 46db413..39846df 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.23.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.23.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.23.0/caching-strategy.md index 3844348..9fca8b5 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.23.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.23.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.23.0/coding-guidelines.md index fb25b96..f3e553e 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.23.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.23.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.23.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.23.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.23.0/debugging.md b/docs/developer/versioned_docs/version-v0.23.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.23.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.23.0/performance.md b/docs/developer/versioned_docs/version-v0.23.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.23.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.23.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.23.0/period-calculation-theory.md index d40776c..2f859b0 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.23.0/period-calculation-theory.md @@ -1092,8 +1092,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.23.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.23.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.23.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.23.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.23.0/timer-architecture.md index ad9dbe4..60da400 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.23.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.23.1/architecture.md b/docs/developer/versioned_docs/version-v0.23.1/architecture.md index b083eb4..5415f0e 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/architecture.md +++ b/docs/developer/versioned_docs/version-v0.23.1/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.23.1/caching-strategy.md b/docs/developer/versioned_docs/version-v0.23.1/caching-strategy.md index 50d9208..5ea8eee 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.23.1/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.23.1/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.23.1/coding-guidelines.md index 7c6d559..96ded5c 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.23.1/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.23.1/critical-patterns.md b/docs/developer/versioned_docs/version-v0.23.1/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.23.1/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.23.1/debugging.md b/docs/developer/versioned_docs/version-v0.23.1/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/debugging.md +++ b/docs/developer/versioned_docs/version-v0.23.1/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.23.1/performance.md b/docs/developer/versioned_docs/version-v0.23.1/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/performance.md +++ b/docs/developer/versioned_docs/version-v0.23.1/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.23.1/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.23.1/period-calculation-theory.md index 861193c..03de7e1 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.23.1/period-calculation-theory.md @@ -1092,8 +1092,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.23.1/repairs-system.md b/docs/developer/versioned_docs/version-v0.23.1/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.23.1/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.23.1/timer-architecture.md b/docs/developer/versioned_docs/version-v0.23.1/timer-architecture.md index bf8331e..940ede3 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.23.1/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.24.0/architecture.md b/docs/developer/versioned_docs/version-v0.24.0/architecture.md index 9dd49d8..33cd2ff 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.24.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.24.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.24.0/caching-strategy.md index c296585..ec0dcb5 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.24.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.24.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.24.0/coding-guidelines.md index c4241d7..e26e0fd 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.24.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.24.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.24.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.24.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.24.0/debugging.md b/docs/developer/versioned_docs/version-v0.24.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.24.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.24.0/performance.md b/docs/developer/versioned_docs/version-v0.24.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.24.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.24.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.24.0/period-calculation-theory.md index b5d8cc1..77f1690 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.24.0/period-calculation-theory.md @@ -1092,8 +1092,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.24.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.24.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.24.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.24.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.24.0/timer-architecture.md index cc6547c..223b5d6 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.24.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.27.0/architecture.md b/docs/developer/versioned_docs/version-v0.27.0/architecture.md index 84b0911..734d5ca 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.27.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.27.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.27.0/caching-strategy.md index 29923f9..630bb5f 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.27.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.27.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.27.0/coding-guidelines.md index cd70a8c..e905cb4 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.27.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.27.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.27.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.27.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.27.0/debugging.md b/docs/developer/versioned_docs/version-v0.27.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.27.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.27.0/performance.md b/docs/developer/versioned_docs/version-v0.27.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.27.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.27.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.27.0/period-calculation-theory.md index f032cc0..12e1277 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.27.0/period-calculation-theory.md @@ -1092,8 +1092,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.27.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.27.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.27.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.27.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.27.0/timer-architecture.md index 60c9b22..b5c425a 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.27.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.28.0/architecture.md b/docs/developer/versioned_docs/version-v0.28.0/architecture.md index d7ed0d1..abea658 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.28.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.28.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.28.0/caching-strategy.md index 68023f0..b64d844 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.28.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.28.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.28.0/coding-guidelines.md index 873c893..566ebf4 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.28.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.28.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.28.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.28.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.28.0/debugging.md b/docs/developer/versioned_docs/version-v0.28.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.28.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.28.0/performance.md b/docs/developer/versioned_docs/version-v0.28.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.28.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.28.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.28.0/period-calculation-theory.md index dda7ad9..0bcaa7b 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.28.0/period-calculation-theory.md @@ -1246,8 +1246,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.28.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.28.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.28.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.28.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.28.0/timer-architecture.md index d9a88cb..5964b1c 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.28.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.29.0/architecture.md b/docs/developer/versioned_docs/version-v0.29.0/architecture.md index ddc5cb2..2010f37 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.29.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.29.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.29.0/caching-strategy.md index 8c23138..6fe1f8e 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.29.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.29.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.29.0/coding-guidelines.md index 13f7adb..cfa0a08 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.29.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.29.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.29.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.29.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.29.0/debugging.md b/docs/developer/versioned_docs/version-v0.29.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.29.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.29.0/performance.md b/docs/developer/versioned_docs/version-v0.29.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.29.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.29.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.29.0/period-calculation-theory.md index 482090b..1010214 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.29.0/period-calculation-theory.md @@ -1246,8 +1246,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.29.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.29.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.29.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.29.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.29.0/timer-architecture.md index 4d2354c..2b40cf9 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.29.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.30.0/architecture.md b/docs/developer/versioned_docs/version-v0.30.0/architecture.md index 84868ec..8b12125 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.30.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.30.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.30.0/caching-strategy.md index 8668c97..6b56262 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.30.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.30.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.30.0/coding-guidelines.md index 8e638d9..3a7ed2a 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.30.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.30.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.30.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.30.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.30.0/debugging.md b/docs/developer/versioned_docs/version-v0.30.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.30.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.30.0/performance.md b/docs/developer/versioned_docs/version-v0.30.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.30.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.30.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.30.0/period-calculation-theory.md index a8464a8..bb63815 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.30.0/period-calculation-theory.md @@ -1246,8 +1246,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.30.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.30.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.30.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.30.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.30.0/timer-architecture.md index ea05a87..d582a2b 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.30.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute) diff --git a/docs/developer/versioned_docs/version-v0.31.0/architecture.md b/docs/developer/versioned_docs/version-v0.31.0/architecture.md index f71bd03..0ec3712 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.31.0/architecture.md @@ -263,20 +263,16 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: ```python # Original from Tibber API -{ - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL" -} +{"startsAt": "2025-11-03T14:00:00+01:00", "total": 0.2534, "level": "NORMAL"} # After enrichment (utils/price.py) { - "startsAt": "2025-11-03T14:00:00+01:00", - "total": 0.2534, - "level": "NORMAL", - "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average - "difference": 9.6, # ← Added: % diff from trailing avg - "rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds + "startsAt": "2025-11-03T14:00:00+01:00", + "total": 0.2534, + "level": "NORMAL", + "trailing_avg_24h": 0.2312, # ← Added: 24h trailing average + "difference": 9.6, # ← Added: % diff from trailing avg + "rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds } ``` diff --git a/docs/developer/versioned_docs/version-v0.31.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.31.0/caching-strategy.md index f939969..893d88a 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.31.0/caching-strategy.md @@ -123,7 +123,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, - "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} + "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, } ``` @@ -178,11 +178,11 @@ description = get_translation("binary_sensor.best_price_period.description", "en ```python hash_data = ( - today_signature, # (startsAt, rating_level) for each interval + today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config - best_level_filter, # Level filter overrides - peak_level_filter + best_level_filter, # Level filter overrides + peak_level_filter, ) ``` diff --git a/docs/developer/versioned_docs/version-v0.31.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.31.0/coding-guidelines.md index ccbfb28..8350b6b 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.31.0/coding-guidelines.md @@ -61,8 +61,10 @@ If a helper class is ONLY used within a single module file, prefix it with under # ✅ Private class - used only in this file class _InternalHelper: """Helper used only within this module.""" + pass + # ❌ Wrong - no prefix but used across modules class DataFetcher: # Should be TibberPricesDataFetcher pass diff --git a/docs/developer/versioned_docs/version-v0.31.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.31.0/critical-patterns.md index f8693a5..d7ba7b9 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.31.0/critical-patterns.md @@ -216,7 +216,7 @@ These patterns were analyzed and classified as **not critical**: coordinator.data = { "user_data": {...}, "priceInfo": [...], # Flat list of all enriched intervals - "currency": "EUR" # Top-level for easy access + "currency": "EUR", # Top-level for easy access } ``` diff --git a/docs/developer/versioned_docs/version-v0.31.0/debugging.md b/docs/developer/versioned_docs/version-v0.31.0/debugging.md index 1f0f4ea..d2a074c 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.31.0/debugging.md @@ -119,7 +119,7 @@ def test_something(coordinator): ```python def test_periods(hass, coordinator): - periods = coordinator.data.get('best_price_periods', []) + periods = coordinator.data.get("best_price_periods", []) for period in periods: print(f"Period: {period['start']} to {period['end']}") print(f" Intervals: {len(period['intervals'])}") @@ -147,15 +147,14 @@ grep "tibber_prices" config/home-assistant.log ```python # In Developer Tools > Template -{{ states.sensor.tibber_home_current_interval_price.last_updated }} +{{states.sensor.tibber_home_current_interval_price.last_updated}} ``` **Debug in code:** ```python # Add logging in sensor/core.py -_LOGGER.debug("Updating sensor %s: old=%s new=%s", - self.entity_id, self._attr_native_value, new_value) +_LOGGER.debug("Updating sensor %s: old=%s new=%s", self.entity_id, self._attr_native_value, new_value) ``` ### Period Calculation Wrong @@ -164,8 +163,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ```python # coordinator/period_handlers/period_building.py -_LOGGER.debug("Candidate intervals: %s", - [(i['startsAt'], i['total']) for i in candidates]) +_LOGGER.debug("Candidate intervals: %s", [(i["startsAt"], i["total"]) for i in candidates]) ``` **Check filter statistics:** @@ -217,6 +215,7 @@ Add to coordinator code: ```python import debugpy + debugpy.listen(5678) _LOGGER.info("Waiting for debugger attach on port 5678") debugpy.wait_for_client() @@ -236,6 +235,7 @@ Add breakpoint: ```python from IPython import embed + embed() # Drops into interactive shell ``` diff --git a/docs/developer/versioned_docs/version-v0.31.0/performance.md b/docs/developer/versioned_docs/version-v0.31.0/performance.md index 4208728..5c1baf8 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.31.0/performance.md @@ -45,8 +45,7 @@ import tracemalloc tracemalloc.start() # Run your code current, peak = tracemalloc.get_traced_memory() -_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", - current / 1024**2, peak / 1024**2) +_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB", current / 1024**2, peak / 1024**2) tracemalloc.stop() ``` @@ -78,6 +77,7 @@ data = await store.async_load() # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} + def get_translation(path: str, language: str) -> dict: cache_key = f"{path}_{language}" if cache_key not in _TRANSLATION_CACHE: @@ -139,10 +139,7 @@ user_data = await fetch_user_data() price_data = await fetch_price_data() # ✅ Concurrent (fast) -user_data, price_data = await asyncio.gather( - fetch_user_data(), - fetch_price_data() -) +user_data, price_data = await asyncio.gather(fetch_user_data(), fetch_price_data()) ``` **2. Don't block event loop:** @@ -175,6 +172,7 @@ class Coordinator: ```python import weakref + class Manager: def __init__(self): self._callbacks: list[weakref.ref] = [] @@ -216,8 +214,7 @@ results = (x for x in items if condition(x)) **Monitor API usage:** ```python -_LOGGER.debug("API call: %s (cache_age=%s)", - endpoint, cache_age) +_LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) ``` ### Smart Updates @@ -243,13 +240,13 @@ async def _async_update_data(self) -> dict: ```python # ❌ MEASUREMENT for prices (stores every change) -state_class=SensorStateClass.MEASUREMENT # ~35K records/year +state_class = SensorStateClass.MEASUREMENT # ~35K records/year # ✅ None for prices (no long-term stats) -state_class=None # Only current state +state_class = None # Only current state # ✅ TOTAL for counters only -state_class=SensorStateClass.TOTAL # For cumulative values +state_class = SensorStateClass.TOTAL # For cumulative values ``` ### Attribute Size @@ -260,7 +257,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values # ❌ Large nested structures (KB per update) attributes = { "all_intervals": [...], # 384 intervals - "full_history": [...], # Days of data + "full_history": [...], # Days of data } # ✅ Essential data only (bytes per update) @@ -279,6 +276,7 @@ attributes = { import pytest import time + @pytest.mark.benchmark def test_period_calculation_performance(coordinator): """Period calculation should complete in <100ms.""" diff --git a/docs/developer/versioned_docs/version-v0.31.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.31.0/period-calculation-theory.md index b6af77a..75e6a07 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.31.0/period-calculation-theory.md @@ -1336,8 +1336,8 @@ for price_data in all_prices: ref_date = date_key criteria = TibberPricesIntervalCriteria( - ref_price=ref_prices[ref_date], # Interval's day - avg_price=avg_prices[ref_date], # Interval's day + ref_price=ref_prices[ref_date], # Interval's day + avg_price=avg_prices[ref_date], # Interval's day flex=flex, min_distance_from_avg=min_distance_from_avg, reverse_sort=reverse_sort, diff --git a/docs/developer/versioned_docs/version-v0.31.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.31.0/repairs-system.md index 36f7314..e8bb82d 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.31.0/repairs-system.md @@ -69,11 +69,7 @@ API requests are being throttled, causing stale data. Updates may be delayed unt ```python # In error handler -is_rate_limit = ( - "429" in error_str - or "rate limit" in error_str - or "too many requests" in error_str -) +is_rate_limit = "429" in error_str or "rate limit" in error_str or "too many requests" in error_str if is_rate_limit: await self._repair_manager.track_rate_limit_error() @@ -295,6 +291,7 @@ async def check_new_condition(self, *, param: bool) -> None: elif not should_warn and self._new_repair_active: await self._clear_new_repair() + async def _create_new_repair(self) -> None: """Create new repair issue.""" _LOGGER.warning("New issue detected - creating repair") @@ -312,6 +309,7 @@ async def _create_new_repair(self) -> None: ) self._new_repair_active = True + async def _clear_new_repair(self) -> None: """Clear new repair issue.""" _LOGGER.debug("New issue resolved - clearing repair") diff --git a/docs/developer/versioned_docs/version-v0.31.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.31.0/timer-architecture.md index 4861adc..a74db53 100644 --- a/docs/developer/versioned_docs/version-v0.31.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.31.0/timer-architecture.md @@ -187,9 +187,8 @@ async def _handle_minute_refresh(self, now: datetime) -> None: class TibberPricesSensor(CoordinatorEntity): async def async_added_to_hass(self): # Register this entity's update callback - self._remove_listener = self.coordinator.async_add_time_sensitive_listener( - self._handle_coordinator_update - ) + self._remove_listener = self.coordinator.async_add_time_sensitive_listener(self._handle_coordinator_update) + # Coordinator maintains list of listeners class ListenerManager: @@ -390,8 +389,8 @@ _LOGGER.setLevel(logging.DEBUG) # Watch for these log messages: "Fetching data from API (reason: tomorrow_check)" # API call -"Using cached data (no update needed)" # Fast path -"Midnight turnover detected (Timer #1)" # Turnover +"Using cached data (no update needed)" # Fast path +"Midnight turnover detected (Timer #1)" # Turnover ``` ### Check Timer #2 (Quarter-Hour) @@ -399,7 +398,8 @@ _LOGGER.setLevel(logging.DEBUG) ```python # Watch coordinator logs: "Updated 60 time-sensitive entities at quarter-hour boundary" # Normal -"Midnight turnover detected (Timer #2)" # Turnover + +"Midnight turnover detected (Timer #2)" # Turnover ``` ### Check Timer #3 (Minute)