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)