chore(docs): format Python code blocks in markdown files with ruff

Run `ruff format .` to reformat Python code blocks inside markdown files
(AGENTS.md and docs/**/*.md). Ruff 0.16.0 formats embedded Python code
blocks in .md files, and the CI `lint-check` step runs `ruff format . --check`
which was failing on 109 files.

Release-Notes: skip
User-Impact: none

Agent-Logs-Url: https://github.com/jpawlowski/hass.tibber_prices/sessions/05437696-813b-432a-b05e-af698a9fa903

Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>
This commit is contained in:
anthropic-code-agent[bot] 2026-07-27 16:50:24 +00:00 committed by GitHub
parent 9c8efc73cf
commit 8adefc7a7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
109 changed files with 543 additions and 613 deletions

View file

@ -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
@ -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 = '<div class="container">content</div>'
# ❌ 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
@ -2132,32 +2139,26 @@ attributes = {
"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)
# 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
"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)
@ -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",
}
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```

View file

@ -263,11 +263,7 @@ 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)
{
@ -276,7 +272,7 @@ All quarter-hourly price intervals get augmented via `utils/price.py`:
"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
"rating_level": "NORMAL", # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```

View file

@ -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},
}
```
@ -182,7 +182,7 @@ hash_data = (
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
peak_level_filter,
)
```

View file

@ -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

View file

@ -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
}
```

View file

@ -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'])}")
@ -154,8 +154,7 @@ grep "tibber_prices" config/home-assistant.log
```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
```

View file

@ -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
@ -279,6 +276,7 @@ attributes = {
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""

View file

@ -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")

View file

@ -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:
@ -399,6 +398,7 @@ _LOGGER.setLevel(logging.DEBUG)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```