refactor(sensors): rename current price sensors for clarity

Renamed internal sensor keys to be more explicit about their temporal scope:
- current_price → current_interval_price
- price_level → current_interval_price_level
- price_rating → current_interval_price_rating

This naming makes it clearer that these sensors represent the current
15-minute interval, distinguishing them from hourly averages and other
time-based calculations.

Updated across all components:
- Sensor entity descriptions and handlers (sensor.py)
- Time-sensitive entity keys list (coordinator.py)
- Config flow step IDs (config_flow.py)
- Translation keys in all 5 languages (de, en, nb, nl, sv)
- Custom translations (entity descriptions, usage tips)
- Price level/rating lookups (const.py, sensor.py)
- Documentation examples (AGENTS.md, README.md)

Impact: Sensor entity IDs remain unchanged due to translation_key system.
Existing automations continue to work. Only internal code references and
translation structures updated for consistency.
This commit is contained in:
Julian Pawlowski 2025-11-15 08:30:25 +00:00
parent c4f36d04de
commit 7737dccd49
17 changed files with 199 additions and 178 deletions

View file

@ -1150,6 +1150,7 @@ We use **Ruff** (which replaces Black, Flake8, isort, and more) as our sole lint
**Critical principle:** Logs must enable logic tracing without reading code. Each log message should make the current state and decision-making process crystal clear.
**Why good logging matters beyond debugging:**
- Clear logs become the foundation for good documentation (see "Documentation Writing Strategy")
- If you spend hours making logs explain the logic, that clarity transfers directly to user docs
- Logs show state transitions and decisions that users need to understand
@ -1158,6 +1159,7 @@ We use **Ruff** (which replaces Black, Flake8, isort, and more) as our sole lint
**Log Level Strategy:**
- **INFO Level** - User-facing results and high-level progress:
- Compact 1-line summaries (no multi-line blocks)
- Important results only (success/failure outcomes)
- No indentation (scannability)
@ -1165,6 +1167,7 @@ We use **Ruff** (which replaces Black, Flake8, isort, and more) as our sole lint
- Example: `"Day 2025-11-11: Success after 1 relaxation phase (2 periods)"`
- **DEBUG Level** - Detailed execution trace:
- Full context headers with all relevant configuration
- Step-by-step progression through logic
- Hierarchical indentation to show call depth/logic structure
@ -1201,6 +1204,7 @@ _LOGGER.debug("%sExtended baseline period from %s to %s", INDENT_L4, old_end, ne
```
**Why indentation?**
- Makes call stack and decision tree visible at a glance
- Enables quick problem localization (which phase/step failed?)
- Shows parent-child relationships between operations
@ -1328,6 +1332,7 @@ When writing or updating user-facing documentation (`docs/user/`), follow these
Understanding **how** good documentation emerges is as important as knowing what makes it good:
- **Live Understanding vs. Code Analysis**
- ✅ **DO:** Write docs during/after active development
- When implementing complex logic, document it while the "why" is fresh
- Use real examples from debugging sessions (actual logs, real data)
@ -1338,6 +1343,7 @@ Understanding **how** good documentation emerges is as important as knowing what
- No user perspective: What's actually confusing?
- **User Feedback Loop**
- Key insight: Documentation improves when users question it
- Pattern:
1. User asks: "Does this still match the code?"
@ -1347,16 +1353,19 @@ Understanding **how** good documentation emerges is as important as knowing what
- Why it works: User questions force critical thinking, real confusion points get addressed
- **Log-Driven Documentation**
- Observation: When logs explain logic clearly, documentation becomes easier
- Why: Logs show state transitions ("Baseline insufficient → Starting relaxation"), decisions ("Replaced period X with larger Y"), and are already written for humans
- Pattern: If you spent hours making logs clear → use that clarity in documentation too
- **Concrete Examples > Abstract Descriptions**
- ✅ **Good:** "Day 2025-11-11 found 2 periods at flex=12.0% +volatility_any (stopped early, no need to try higher flex)"
- ❌ **Bad:** "The relaxation algorithm uses a configurable threshold multiplier with filter combination strategies"
- Use real data from debug sessions, show actual attribute values, demonstrate with timeline diagrams
- **Context Accumulation in Long Sessions**
- Advantage: AI builds mental model incrementally, sees evolution of logic (not just final state), understands trade-offs
- Disadvantage of short sessions: Cold start every time, missing "why" context, documentation becomes spec-writing
- Lesson: Complex documentation benefits from focused, uninterrupted work with accumulated context
@ -1406,7 +1415,7 @@ name = 'tibber_prices' # Ruff will change to double quotes
```python
# ✅ Always use trailing commas in multi-line structures
SENSOR_TYPES = [
"current_price",
"current_interval_price",
"min_price",
"max_price", # ← Trailing comma
]
@ -1421,7 +1430,7 @@ def calculate_average(
# ❌ Missing trailing comma
SENSOR_TYPES = [
"current_price",
"current_interval_price",
"min_price",
"max_price" # Ruff will add trailing comma
]
@ -1483,7 +1492,7 @@ df = (
```python
# ✅ Annotate function signatures (public functions)
def get_current_price(coordinator: DataUpdateCoordinator) -> float:
def get_current_interval_price(coordinator: DataUpdateCoordinator) -> float:
"""Get current price from coordinator."""
return coordinator.data["priceInfo"]["today"][0]["total"]

View file

@ -44,6 +44,7 @@ Click the button below to open the integration directly in HACS:
[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
Then:
1. Click "Download" to install the integration
2. **Restart Home Assistant** (required after installation)
@ -60,6 +61,7 @@ Click the button below to open the configuration dialog:
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=tibber_prices)
This will guide you through:
1. Enter your Tibber API token ([get one here](https://developer.tibber.com/settings/access-token))
2. Select your Tibber home
3. Configure price thresholds (optional)
@ -88,7 +90,7 @@ The integration provides **30+ sensors** across different categories. Key sensor
### Core Price Sensors (Enabled by Default)
| Entity | Description |
| ----------------------------- | ------------------------------------------------- |
| -------------------------- | ------------------------------------------------- |
| Current Electricity Price | Current 15-minute interval price |
| Next Interval Price | Price for the next 15-minute interval |
| Current Hour Average Price | Average of current hour's 4 intervals |
@ -101,7 +103,7 @@ The integration provides **30+ sensors** across different categories. Key sensor
### Statistical Sensors (Enabled by Default)
| Entity | Description |
| ------------------------------ | -------------------------------------------- |
| ------------------------- | ------------------------------------------- |
| Today's Lowest Price | Minimum price for today |
| Today's Highest Price | Maximum price for today |
| Today's Average Price | Mean price across today's intervals |
@ -115,7 +117,7 @@ The integration provides **30+ sensors** across different categories. Key sensor
### Price Rating Sensors (Enabled by Default)
| Entity | Description |
| --------------------------- | ---------------------------------------------------------- |
| -------------------------- | --------------------------------------------------------- |
| Current Price Rating | % difference from 24h trailing average (current interval) |
| Next Interval Price Rating | % difference from 24h trailing average (next interval) |
| Current Hour Price Rating | % difference for current hour average |
@ -126,7 +128,7 @@ The integration provides **30+ sensors** across different categories. Key sensor
### Binary Sensors (Enabled by Default)
| Entity | Description |
| -------------------------- | -------------------------------------------------------------- |
| ------------------------- | ----------------------------------------------------------------------------------------- |
| Peak Price Period | ON when in a detected peak price period ([how it works](docs/user/period-calculation.md)) |
| Best Price Period | ON when in a detected best price period ([how it works](docs/user/period-calculation.md)) |
| Tibber API Connection | Connection status to Tibber API |
@ -135,7 +137,7 @@ The integration provides **30+ sensors** across different categories. Key sensor
### Diagnostic Sensors (Enabled by Default)
| Entity | Description |
| ----------------- | ------------------------------------------ |
| --------------- | ------------------------------------------ |
| Data Expiration | Timestamp when current data expires |
| Price Forecast | Formatted list of upcoming price intervals |
@ -186,7 +188,7 @@ automation:
- alias: "Notify on Very Expensive Electricity"
trigger:
- platform: state
entity_id: sensor.tibber_current_price_level
entity_id: sensor.tibber_current_interval_price_level
to: "VERY_EXPENSIVE"
action:
- service: notify.mobile_app
@ -204,7 +206,7 @@ automation:
- alias: "Reduce Heating During High Price Ratings"
trigger:
- platform: numeric_state
entity_id: sensor.tibber_current_price_rating
entity_id: sensor.tibber_current_interval_price_rating
above: 20 # More than 20% above 24h average
action:
- service: climate.set_temperature
@ -227,7 +229,7 @@ automation:
to: "on"
condition:
- condition: numeric_state
entity_id: sensor.tibber_current_price_rating
entity_id: sensor.tibber_current_interval_price_rating
below: -15 # At least 15% below average
- condition: numeric_state
entity_id: sensor.ev_battery_level
@ -273,15 +275,18 @@ automation:
Every sensor includes rich attributes beyond just the state value. These attributes provide context, timestamps, and additional data useful for automations and templates.
**Standard attributes available on most sensors:**
- `timestamp` - ISO 8601 timestamp for the data point
- `description` - Brief explanation of what the sensor represents
- `level_id` and `level_value` - For price level sensors (e.g., `VERY_CHEAP` = -2)
**Extended descriptions** (enable in integration options):
- `long_description` - Detailed explanation of the sensor's purpose
- `usage_tips` - Practical suggestions for using the sensor in automations
**Example - Current Price sensor attributes:**
```yaml
timestamp: "2025-11-03T14:15:00+01:00"
description: "The current electricity price per kWh"
@ -290,6 +295,7 @@ usage_tips: "Use this to track prices or to create automations that run when ele
```
**Example template using attributes:**
```yaml
template:
- sensor:

View file

@ -488,7 +488,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
_TOTAL_STEPS: ClassVar[int] = 6
_STEP_INFO: ClassVar[dict[str, int]] = {
"init": 1,
"price_rating": 2,
"current_interval_price_rating": 2,
"volatility": 3,
"best_price": 4,
"peak_price": 5,
@ -553,7 +553,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
return await self.async_step_volatility()
return self.async_show_form(
step_id="price_rating",
step_id="current_interval_price_rating",
data_schema=vol.Schema(
{
vol.Optional(
@ -592,7 +592,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
),
}
),
description_placeholders=self._get_step_description_placeholders("price_rating"),
description_placeholders=self._get_step_description_placeholders("current_interval_price_rating"),
)
async def async_step_best_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
@ -666,7 +666,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
SelectSelectorConfig(
options=BEST_PRICE_MAX_LEVEL_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="price_level",
translation_key="current_interval_price_level",
),
),
vol.Optional(
@ -817,7 +817,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
SelectSelectorConfig(
options=PEAK_PRICE_MIN_LEVEL_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="price_level",
translation_key="current_interval_price_level",
),
),
vol.Optional(

View file

@ -267,7 +267,7 @@ PRICE_LEVEL_COLOR_MAPPING = {
}
# Icon mapping for current price sensors (dynamic icons based on price level)
# Used by current_price and current_hour_average sensors
# Used by current_interval_price and current_hour_average sensors
# Icon shows price level (cheap/normal/expensive), icon_color reinforces with color
PRICE_LEVEL_CASH_ICON_MAPPING = {
PRICE_LEVEL_VERY_CHEAP: "mdi:cash-multiple", # Many coins (save a lot!)
@ -667,7 +667,9 @@ async def async_get_price_level_translation(
The localized price level if found, None otherwise
"""
return await async_get_translation(hass, ["sensor", "price_level", "price_levels", level], language)
return await async_get_translation(
hass, ["sensor", "current_interval_price_level", "price_levels", level], language
)
def get_price_level_translation(
@ -687,7 +689,7 @@ def get_price_level_translation(
The localized price level if found in cache, None otherwise
"""
return get_translation(["sensor", "price_level", "price_levels", level], language)
return get_translation(["sensor", "current_interval_price_level", "price_levels", level], language)
async def async_get_home_type_translation(

View file

@ -107,11 +107,11 @@ TOMORROW_DATA_CHECK_HOUR = 13
TIME_SENSITIVE_ENTITY_KEYS = frozenset(
{
# Current/next/previous price sensors
"current_price",
"current_interval_price",
"next_interval_price",
"previous_interval_price",
# Current/next/previous price levels
"price_level",
"current_interval_price_level",
"next_interval_price_level",
"previous_interval_price_level",
# Rolling hour calculations (5-interval windows)
@ -120,7 +120,7 @@ TIME_SENSITIVE_ENTITY_KEYS = frozenset(
"current_hour_price_level",
"next_hour_price_level",
# Current/next/previous price ratings
"price_rating",
"current_interval_price_rating",
"next_interval_price_rating",
"previous_interval_price_rating",
"current_hour_price_rating",
@ -222,7 +222,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""
Listen for time-sensitive updates that occur every quarter-hour.
Time-sensitive entities (like current_price, next_interval_price, etc.) should use this
Time-sensitive entities (like current_interval_price, next_interval_price, etc.) should use this
method instead of async_add_listener to receive updates at quarter-hour boundaries.
Returns:

View file

@ -1,6 +1,6 @@
{
"sensor": {
"current_price": {
"current_interval_price": {
"description": "Der aktuelle Strompreis pro kWh",
"long_description": "Zeigt den aktuellen Preis pro kWh von deinem Tibber-Abonnement an",
"usage_tips": "Nutze dies, um Preise zu verfolgen oder Automatisierungen zu erstellen, die bei günstigem Strom ausgeführt werden"
@ -85,7 +85,7 @@
"long_description": "Zeigt den höchsten Preis pro kWh für die nächsten 24 Stunden (vorlaufendes Maximum) von deinem Tibber-Abonnement an. Dies bietet den höchsten erwarteten Preis in den nächsten 24 Stunden basierend auf Prognosedaten.",
"usage_tips": "Nutze dies, um den Betrieb von Geräten während kommender Spitzenpreiszeiten zu vermeiden."
},
"price_level": {
"current_interval_price_level": {
"description": "Die aktuelle Preislevelklassifikation",
"long_description": "Zeigt die Klassifizierung von Tibber für den aktuellen Preis im Vergleich zu historischen Preisen an",
"usage_tips": "Nutze dies, um Automatisierungen auf Basis des relativen Preisniveaus anstelle der absoluten Preise zu erstellen"
@ -110,7 +110,7 @@
"long_description": "Zeigt das mediane Preisniveau über 5 Intervalle, die eine Stunde voraus zentriert sind. Hilft bei der Verbrauchsplanung basierend auf kommenden Preistrends statt momentanen zukünftigen Preisen.",
"usage_tips": "Nutze dies, um Aktivitäten für die nächste Stunde basierend auf einer geglätteten Preisniveau-Prognose zu planen."
},
"price_rating": {
"current_interval_price_rating": {
"description": "Wie sich der Preis des aktuellen Intervalls mit historischen Daten vergleicht",
"long_description": "Zeigt, wie sich der Preis des aktuellen Intervalls im Vergleich zu historischen Preisdaten als Prozentsatz verhält",
"usage_tips": "Ein positiver Prozentsatz bedeutet, dass der aktuelle Preis überdurchschnittlich ist, negativ bedeutet unterdurchschnittlich"

View file

@ -1,6 +1,6 @@
{
"sensor": {
"current_price": {
"current_interval_price": {
"description": "The current electricity price per kWh",
"long_description": "Shows the current price per kWh from your Tibber subscription",
"usage_tips": "Use this to track prices or to create automations that run when electricity is cheap"
@ -85,7 +85,7 @@
"long_description": "Shows the maximum price per kWh from the next 24 hours (leading maximum) from your Tibber subscription. This provides the highest price expected in the next 24 hours based on forecast data.",
"usage_tips": "Use this to avoid running appliances during upcoming peak price periods."
},
"price_level": {
"current_interval_price_level": {
"description": "The current price level classification",
"long_description": "Shows Tibber's classification of the current price compared to historical prices",
"usage_tips": "Use this to create automations based on relative price levels rather than absolute prices"
@ -110,7 +110,7 @@
"long_description": "Shows the median price level across 5 intervals centered one hour ahead. Helps plan consumption based on upcoming price trends rather than instantaneous future prices.",
"usage_tips": "Use to schedule activities for the next hour based on a smoothed price level forecast."
},
"price_rating": {
"current_interval_price_rating": {
"description": "How the current interval's price compares to historical data",
"long_description": "Shows how the current interval's price compares to historical price data as a percentage",
"usage_tips": "A positive percentage means the current price is above average, negative means below average"

View file

@ -1,6 +1,6 @@
{
"sensor": {
"current_price": {
"current_interval_price": {
"description": "Den nåværende elektrisitetsprisen per kWh",
"long_description": "Viser nåværende pris per kWh fra ditt Tibber-abonnement",
"usage_tips": "Bruk dette til å spore priser eller lage automatiseringer som kjører når strøm er billig"
@ -85,7 +85,7 @@
"long_description": "Viser maksimumsprisen per kWh fra de neste 24 timene (fremtidsrettet maksimum) fra ditt Tibber-abonnement. Dette gir den høyeste prisen forventet i de neste 24 timene basert på prognosedata.",
"usage_tips": "Bruk dette til å unngå å kjøre apparater i kommende topprisperioder."
},
"price_level": {
"current_interval_price_level": {
"description": "Den nåværende prisnivåklassifiseringen",
"long_description": "Viser Tibbers klassifisering av nåværende pris sammenlignet med historiske priser",
"usage_tips": "Bruk dette til å lage automatiseringer basert på relative prisnivåer i stedet for absolutte priser"
@ -110,7 +110,7 @@
"long_description": "Viser median prisnivå på tvers av 5 intervaller sentrert en time frem. Hjelper med å planlegge forbruk basert på kommende pristrender i stedet for øyeblikkelige fremtidige priser.",
"usage_tips": "Bruk for å planlegge aktiviteter for neste time basert på en utjevnet prisnivåprognose."
},
"price_rating": {
"current_interval_price_rating": {
"description": "Hvordan nåværende intervalls pris sammenlignes med historiske data",
"long_description": "Viser hvordan nåværende intervalls pris sammenlignes med historiske prisdata som en prosentandel",
"usage_tips": "En positiv prosentandel betyr at nåværende pris er over gjennomsnittet, negativ betyr under gjennomsnittet"

View file

@ -1,6 +1,6 @@
{
"sensor": {
"current_price": {
"current_interval_price": {
"description": "De huidige elektriciteitsprijs per kWh",
"long_description": "Toont de huidige prijs per kWh van uw Tibber-abonnement",
"usage_tips": "Gebruik dit om prijzen bij te houden of om automatiseringen te maken die worden uitgevoerd wanneer elektriciteit goedkoop is"
@ -85,7 +85,7 @@
"long_description": "Toont de maximumprijs per kWh van de komende 24 uur (vooruitlopend maximum) van uw Tibber-abonnement. Dit geeft de hoogste prijs die wordt verwacht in de komende 24 uur op basis van prognosegegevens.",
"usage_tips": "Gebruik dit om te voorkomen dat u apparaten draait tijdens aanstaande piekprijsperioden."
},
"price_level": {
"current_interval_price_level": {
"description": "De huidige prijsniveauclassificatie",
"long_description": "Toont de classificatie van Tibber van de huidige prijs vergeleken met historische prijzen",
"usage_tips": "Gebruik dit om automatiseringen te maken op basis van relatieve prijsniveaus in plaats van absolute prijzen"
@ -110,7 +110,7 @@
"long_description": "Toont het mediane prijsniveau over 5 intervallen gecentreerd één uur vooruit. Helpt verbruik te plannen op basis van aanstaande prijstrends in plaats van momentane toekomstige prijzen.",
"usage_tips": "Gebruik om activiteiten voor het volgende uur te plannen op basis van een vloeiende prijsniveauprognose."
},
"price_rating": {
"current_interval_price_rating": {
"description": "Hoe de prijs van het huidige interval zich verhoudt tot historische gegevens",
"long_description": "Toont hoe de prijs van het huidige interval zich verhoudt tot historische prijsgegevens als een percentage",
"usage_tips": "Een positief percentage betekent dat de huidige prijs boven het gemiddelde ligt, negatief betekent onder het gemiddelde"

View file

@ -1,6 +1,6 @@
{
"sensor": {
"current_price": {
"current_interval_price": {
"description": "Det nuvarande elpriset per kWh",
"long_description": "Visar nuvarande pris per kWh från ditt Tibber-abonnemang",
"usage_tips": "Använd detta för att spåra priser eller skapa automationer som körs när el är billig"
@ -85,7 +85,7 @@
"long_description": "Visar maximipriset per kWh från nästa 24 timmar (framåtblickande maximum) från ditt Tibber-abonnemang. Detta ger det högsta priset som förväntas nästa 24 timmar baserat på prognosdata.",
"usage_tips": "Använd detta för att undvika att köra apparater under kommande toppprisperioder."
},
"price_level": {
"current_interval_price_level": {
"description": "Den nuvarande prisnivåklassificeringen",
"long_description": "Visar Tibbers klassificering av nuvarande pris jämfört med historiska priser",
"usage_tips": "Använd detta för att skapa automationer baserade på relativa prisnivåer istället för absoluta priser"
@ -110,7 +110,7 @@
"long_description": "Visar median prisnivå över 5 intervaller centrerade en timme framåt. Hjälper att planera konsumtion baserat på kommande pristrender istället för ögonblickliga framtida priser.",
"usage_tips": "Använd för att schemalägga aktiviteter för nästa timme baserat på en utjämnad prisnivåprognos."
},
"price_rating": {
"current_interval_price_rating": {
"description": "Hur nuvarande intervalls pris jämförs med historiska data",
"long_description": "Visar hur nuvarande intervalls pris jämförs med historiska prisdata som en procentsats",
"usage_tips": "En positiv procentsats betyder att nuvarande pris är över genomsnittet, negativ betyder under genomsnittet"

View file

@ -149,7 +149,7 @@ def calculate_trailing_average_for_interval(
def calculate_difference_percentage(
current_price: float,
current_interval_price: float,
trailing_average: float | None,
) -> float | None:
"""
@ -158,7 +158,7 @@ def calculate_difference_percentage(
This mimics the API's "difference" field from priceRating endpoint.
Args:
current_price: The current interval's price
current_interval_price: The current interval's price
trailing_average: The 24-hour trailing average price
Returns:
@ -169,7 +169,7 @@ def calculate_difference_percentage(
if trailing_average is None or trailing_average == 0:
return None
return ((current_price - trailing_average) / trailing_average) * 100
return ((current_interval_price - trailing_average) / trailing_average) * 100
def calculate_rating_level(
@ -238,9 +238,9 @@ def _process_price_interval(
return
starts_at = dt_util.as_local(starts_at)
current_price = price_interval.get("total")
current_interval_price = price_interval.get("total")
if current_price is None:
if current_interval_price is None:
return
# Calculate trailing average
@ -248,7 +248,7 @@ def _process_price_interval(
# Calculate and set the difference and rating_level
if trailing_avg is not None:
difference = calculate_difference_percentage(float(current_price), trailing_avg)
difference = calculate_difference_percentage(float(current_interval_price), trailing_avg)
price_interval["difference"] = difference
# Calculate rating_level based on difference
@ -486,7 +486,7 @@ def aggregate_period_ratings(
def calculate_price_trend(
current_price: float,
current_interval_price: float,
future_average: float,
threshold_rising: float = 5.0,
threshold_falling: float = -5.0,
@ -495,7 +495,7 @@ def calculate_price_trend(
Calculate price trend by comparing current price with future average.
Args:
current_price: Current interval price
current_interval_price: Current interval price
future_average: Average price of future intervals
threshold_rising: Percentage threshold for rising trend (positive, default 5%)
threshold_falling: Percentage threshold for falling trend (negative, default -5%)
@ -506,12 +506,12 @@ def calculate_price_trend(
difference_percentage: % change from current to future ((future - current) / current * 100)
"""
if current_price == 0:
if current_interval_price == 0:
# Avoid division by zero
return "stable", 0.0
# Calculate percentage difference from current to future
diff_pct = ((future_average - current_price) / current_price) * 100
diff_pct = ((future_average - current_interval_price) / current_interval_price) * 100
# Determine trend based on thresholds
# threshold_falling is negative, so we compare with it directly

View file

@ -80,8 +80,8 @@ MIN_HOURS_FOR_LATER_HALF = 3 # Minimum hours needed to calculate later half ave
# Main price sensors that users will typically use in automations
PRICE_SENSORS = (
SensorEntityDescription(
key="current_price",
translation_key="current_price",
key="current_interval_price",
translation_key="current_interval_price",
name="Current Electricity Price",
icon="mdi:cash", # Dynamic: will show cash-multiple/plus/cash/minus/remove based on level
device_class=SensorDeviceClass.MONETARY,
@ -124,8 +124,8 @@ PRICE_SENSORS = (
# import timing issues with Home Assistant's entity platform initialization.
# Keep in sync with PRICE_LEVEL_OPTIONS in const.py!
SensorEntityDescription(
key="price_level",
translation_key="price_level",
key="current_interval_price_level",
translation_key="current_interval_price_level",
name="Current Price Level",
icon="mdi:gauge",
device_class=SensorDeviceClass.ENUM,
@ -314,8 +314,8 @@ VOLATILITY_SENSORS = (
# Keep in sync with PRICE_RATING_OPTIONS in const.py!
RATING_SENSORS = (
SensorEntityDescription(
key="price_rating",
translation_key="price_rating",
key="current_interval_price_rating",
translation_key="current_interval_price_rating",
name="Current Price Rating",
icon="mdi:star-outline",
device_class=SensorDeviceClass.ENUM,
@ -620,13 +620,13 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
# Map sensor keys to their handler methods
handlers = {
# Price level sensors
"price_level": self._get_price_level_value,
"current_interval_price_level": self._get_price_level_value,
"next_interval_price_level": lambda: self._get_interval_level_value(interval_offset=1),
"previous_interval_price_level": lambda: self._get_interval_level_value(interval_offset=-1),
"current_hour_price_level": lambda: self._get_rolling_hour_level_value(hour_offset=0),
"next_hour_price_level": lambda: self._get_rolling_hour_level_value(hour_offset=1),
# Price sensors
"current_price": lambda: self._get_interval_price_value(interval_offset=0, in_euro=False),
"current_interval_price": lambda: self._get_interval_price_value(interval_offset=0, in_euro=False),
"next_interval_price": lambda: self._get_interval_price_value(interval_offset=1, in_euro=False),
"previous_interval_price": lambda: self._get_interval_price_value(interval_offset=-1, in_euro=False),
# Rolling hour average (5 intervals: 2 before + current + 2 after)
@ -692,7 +692,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
decimals=2,
),
# Rating sensors
"price_rating": lambda: self._get_rating_value(rating_type="current"),
"current_interval_price_rating": lambda: self._get_rating_value(rating_type="current"),
"next_interval_price_rating": lambda: self._get_interval_rating_value(interval_offset=1),
"previous_interval_price_rating": lambda: self._get_interval_rating_value(interval_offset=-1),
"current_hour_price_rating": lambda: self._get_rolling_hour_rating_value(hour_offset=0),
@ -1115,11 +1115,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
if (
translations
and "sensor" in translations
and "price_rating" in translations["sensor"]
and "price_levels" in translations["sensor"]["price_rating"]
and level in translations["sensor"]["price_rating"]["price_levels"]
and "current_interval_price_rating" in translations["sensor"]
and "price_levels" in translations["sensor"]["current_interval_price_rating"]
and level in translations["sensor"]["current_interval_price_rating"]["price_levels"]
):
return translations["sensor"]["price_rating"]["price_levels"][level]
return translations["sensor"]["current_interval_price_rating"]["price_levels"][level]
# Fallback to English if not found
if language != "en":
en_cache_key = f"{DOMAIN}_translations_en"
@ -1127,11 +1127,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
if (
en_translations
and "sensor" in en_translations
and "price_rating" in en_translations
and "price_levels" in en_translations["sensor"]["price_rating"]
and level in en_translations["sensor"]["price_rating"]["price_levels"]
and "current_interval_price_rating" in en_translations
and "price_levels" in en_translations["sensor"]["current_interval_price_rating"]
and level in en_translations["sensor"]["current_interval_price_rating"]["price_levels"]
):
return en_translations["sensor"]["price_rating"]["price_levels"][level]
return en_translations["sensor"]["current_interval_price_rating"]["price_levels"][level]
return level
def _get_rating_value(self, *, rating_type: str) -> str | None:
@ -1285,7 +1285,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
if not current_interval or "total" not in current_interval:
return None
current_price = float(current_interval["total"])
current_interval_price = float(current_interval["total"])
current_starts_at = dt_util.parse_datetime(current_interval["startsAt"])
if current_starts_at is None:
return None
@ -1311,7 +1311,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
# Calculate trend with configured thresholds
trend_state, diff_pct = calculate_price_trend(
current_price, future_avg, threshold_rising=threshold_rising, threshold_falling=threshold_falling
current_interval_price, future_avg, threshold_rising=threshold_rising, threshold_falling=threshold_falling
)
# Determine icon color based on trend state
@ -1340,8 +1340,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
self._trend_attributes[f"second_half_{hours}h_avg"] = round(later_half_avg * 100, 2)
# Calculate incremental change: how much does the later half differ from current?
if current_price > 0:
later_half_diff = ((later_half_avg - current_price) / current_price) * 100
if current_interval_price > 0:
later_half_diff = ((later_half_avg - current_interval_price) / current_interval_price) * 100
self._trend_attributes[f"second_half_{hours}h_diff_from_current_%"] = round(later_half_diff, 1)
# Cache the trend value for consistency
@ -1711,7 +1711,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
if not self.coordinator.data or not self._value_getter:
return None
# For price_level, ensure we return the translated value as state
if self.entity_description.key == "price_level":
if self.entity_description.key == "current_interval_price_level":
return self._get_price_level_value()
return self._value_getter()
except (KeyError, ValueError, TypeError) as ex:
@ -1771,11 +1771,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
"""
Get icon for current price sensors (dynamic based on price level).
Only current_price and current_hour_average have dynamic icons.
Only current_interval_price and current_hour_average have dynamic icons.
Other price sensors (next/previous) use static icons from entity description.
"""
# Only current price sensors get dynamic icons
if key == "current_price":
if key == "current_interval_price":
level = self._get_price_level_for_sensor(key)
if level:
return PRICE_LEVEL_CASH_ICON_MAPPING.get(level.upper())
@ -1790,7 +1790,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
def _get_level_sensor_icon(self, key: str, value: Any) -> str | None:
"""Get icon for price level sensors."""
if key not in [
"price_level",
"current_interval_price_level",
"next_interval_price_level",
"previous_interval_price_level",
"current_hour_price_level",
@ -1803,7 +1803,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
def _get_rating_sensor_icon(self, key: str, value: Any) -> str | None:
"""Get icon for price rating sensors."""
if key not in [
"price_rating",
"current_interval_price_rating",
"next_interval_price_rating",
"previous_interval_price_rating",
"current_hour_price_rating",
@ -1830,7 +1830,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
# Map sensor key to interval offset
offset_map = {
"current_price": 0,
"current_interval_price": 0,
"next_interval_price": 1,
"previous_interval_price": -1,
}
@ -1988,8 +1988,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
# Group sensors by type and delegate to specific handlers
if key in [
"current_price",
"price_level",
"current_interval_price",
"current_interval_price_level",
"next_interval_price",
"previous_interval_price",
"current_hour_average",
@ -2003,7 +2003,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
"current_hour_price_rating",
"next_hour_price_rating",
]:
self._add_current_price_attributes(attributes)
self._add_current_interval_price_attributes(attributes)
elif key in [
"trailing_price_average",
"leading_price_average",
@ -2022,7 +2022,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
elif key.endswith("_volatility"):
self._add_volatility_attributes(attributes)
# For price_level, add the original level as attribute
if key == "price_level" and hasattr(self, "_last_price_level") and self._last_price_level is not None:
if (
key == "current_interval_price_level"
and hasattr(self, "_last_price_level")
and self._last_price_level is not None
):
attributes["level_id"] = self._last_price_level
except (KeyError, ValueError, TypeError) as ex:
self.coordinator.logger.exception(
@ -2035,8 +2039,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
else:
return attributes if attributes else None
def _add_current_price_attributes(self, attributes: dict) -> None:
"""Add attributes for current price sensors."""
def _add_current_interval_price_attributes(self, attributes: dict) -> None:
"""Add attributes for current interval price sensors."""
key = self.entity_description.key
price_info = self.coordinator.data.get("priceInfo", {}) if self.coordinator.data else {}
now = dt_util.now()
@ -2085,7 +2089,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
# Add icon_color for price sensors (based on their price level)
if key in ["current_price", "next_interval_price", "previous_interval_price"]:
if key in ["current_interval_price", "next_interval_price", "previous_interval_price"]:
# For interval-based price sensors, get level from interval_data
if interval_data and "level" in interval_data:
level = interval_data["level"]
@ -2115,7 +2119,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
if level_value and isinstance(level_value, str):
self._add_price_level_attributes(attributes, level_value.upper())
# For current price level sensor
elif key == "price_level":
elif key == "current_interval_price_level":
current_interval_data = self._get_current_interval_data()
if current_interval_data and "level" in current_interval_data:
self._add_price_level_attributes(attributes, current_interval_data["level"])
@ -2149,7 +2153,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
if rating_value and isinstance(rating_value, str):
self._add_price_rating_attributes(attributes, rating_value.upper())
# For current price rating sensor
elif key == "price_rating":
elif key == "current_interval_price_rating":
current_interval_data = self._get_current_interval_data()
if current_interval_data and "rating_level" in current_interval_data:
self._add_price_rating_attributes(attributes, current_interval_data["rating_level"])
@ -2201,7 +2205,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
latest_timestamp = self._get_data_timestamp()
if latest_timestamp:
attributes["timestamp"] = latest_timestamp.isoformat()
elif key == "price_rating":
elif key == "current_interval_price_rating":
interval_data = find_price_data_for_interval(price_info, now)
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
if hasattr(self, "_last_rating_difference") and self._last_rating_difference is not None:

View file

@ -88,7 +88,7 @@
},
"submit": "Weiter zu Schritt 2"
},
"price_rating": {
"current_interval_price_rating": {
"title": "Preisbewertungs-Schwellwerte",
"description": "{step_progress}\n\nKonfiguration der Schwellwerte für Preisbewertungsstufen (niedrig/normal/hoch) basierend auf dem Vergleich mit dem gleitenden 24-Stunden-Durchschnitt.",
"data": {
@ -183,7 +183,7 @@
},
"entity": {
"sensor": {
"current_price": {
"current_interval_price": {
"name": "Aktueller Strompreis"
},
"next_interval_price": {
@ -198,7 +198,7 @@
"next_hour_average": {
"name": "Nächster Stunden-Durchschnittspreis"
},
"price_level": {
"current_interval_price_level": {
"name": "Aktuelles Preisniveau",
"state": {
"very_cheap": "Sehr günstig",
@ -284,7 +284,7 @@
"leading_price_max": {
"name": "Vorlaufender 24h-Höchstpreis"
},
"price_rating": {
"current_interval_price_rating": {
"name": "Aktuelle Preisbewertung",
"state": {
"low": "Niedrig",
@ -507,7 +507,7 @@
"very_high": "Sehr hoch"
}
},
"price_level": {
"current_interval_price_level": {
"options": {
"any": "Beliebig",
"very_cheap": "Sehr günstig",

View file

@ -88,7 +88,7 @@
},
"submit": "Next to Step 2"
},
"price_rating": {
"current_interval_price_rating": {
"title": "Price Rating Thresholds",
"description": "{step_progress}\n\nConfigure thresholds for price rating levels (low/normal/high) based on comparison with trailing 24-hour average.",
"data": {
@ -179,7 +179,7 @@
},
"entity": {
"sensor": {
"current_price": {
"current_interval_price": {
"name": "Current Electricity Price"
},
"next_interval_price": {
@ -194,7 +194,7 @@
"next_hour_average": {
"name": "Next Hour Average Price"
},
"price_level": {
"current_interval_price_level": {
"name": "Current Price Level",
"state": {
"very_cheap": "Very Cheap",
@ -280,7 +280,7 @@
"leading_price_max": {
"name": "Leading 24h Maximum Price"
},
"price_rating": {
"current_interval_price_rating": {
"name": "Current Price Rating",
"state": {
"low": "Low",
@ -503,7 +503,7 @@
"very_high": "Very high"
}
},
"price_level": {
"current_interval_price_level": {
"options": {
"any": "Any",
"very_cheap": "Very cheap",

View file

@ -88,7 +88,7 @@
},
"submit": "Neste til steg 2"
},
"price_rating": {
"current_interval_price_rating": {
"title": "Prisvurderingsterskler",
"description": "{step_progress}\n\nKonfigurer terskler for prisvurderingsnivåer (lav/normal/høy) basert på sammenligning med 24-timers glidende gjennomsnitt.",
"data": {
@ -179,7 +179,7 @@
},
"entity": {
"sensor": {
"current_price": {
"current_interval_price": {
"name": "Nåværende strømpris"
},
"next_interval_price": {
@ -194,7 +194,7 @@
"next_hour_average": {
"name": "Neste timepris gjennomsnitt"
},
"price_level": {
"current_interval_price_level": {
"name": "Nåværende prisnivå",
"state": {
"very_cheap": "Veldig billig",
@ -280,7 +280,7 @@
"leading_price_max": {
"name": "Fremtidig 24t maksimumspris"
},
"price_rating": {
"current_interval_price_rating": {
"name": "Nåværende prisvurdering",
"state": {
"low": "Lav",
@ -503,7 +503,7 @@
"very_high": "Svært høy"
}
},
"price_level": {
"current_interval_price_level": {
"options": {
"any": "Alle",
"very_cheap": "Svært billig",

View file

@ -88,7 +88,7 @@
},
"submit": "Volgende naar stap 2"
},
"price_rating": {
"current_interval_price_rating": {
"title": "Prijsbeoordelingsdrempels",
"description": "{step_progress}\n\nConfigureer drempels voor prijsbeoordelingsniveaus (laag/normaal/hoog) op basis van vergelijking met het voortschrijdend 24-uurs gemiddelde.",
"data": {
@ -179,7 +179,7 @@
},
"entity": {
"sensor": {
"current_price": {
"current_interval_price": {
"name": "Huidige elektriciteitsprijs"
},
"next_interval_price": {
@ -194,7 +194,7 @@
"next_hour_average": {
"name": "Volgend uurgemiddelde prijs"
},
"price_level": {
"current_interval_price_level": {
"name": "Huidig prijsniveau",
"state": {
"very_cheap": "Zeer goedkoop",
@ -280,7 +280,7 @@
"leading_price_max": {
"name": "Vooruitlopend 24u maximumprijs"
},
"price_rating": {
"current_interval_price_rating": {
"name": "Huidige prijsbeoordeling",
"state": {
"low": "Laag",
@ -503,7 +503,7 @@
"very_high": "Zeer hoog"
}
},
"price_level": {
"current_interval_price_level": {
"options": {
"any": "Alle",
"very_cheap": "Zeer goedkoop",

View file

@ -88,7 +88,7 @@
},
"submit": "Nästa till steg 2"
},
"price_rating": {
"current_interval_price_rating": {
"title": "Prisvärderingströsklar",
"description": "{step_progress}\n\nKonfigurera trösklar för prisvärderingsnivåer (låg/normal/hög) baserat på jämförelse med rullande 24-timmars genomsnitt.",
"data": {
@ -179,7 +179,7 @@
},
"entity": {
"sensor": {
"current_price": {
"current_interval_price": {
"name": "Nuvarande elpris"
},
"next_interval_price": {
@ -194,7 +194,7 @@
"next_hour_average": {
"name": "Nästa timgenomsnitt pris"
},
"price_level": {
"current_interval_price_level": {
"name": "Nuvarande prisnivå",
"state": {
"very_cheap": "Mycket billigt",
@ -280,7 +280,7 @@
"leading_price_max": {
"name": "Framåtblickande 24t maximumpris"
},
"price_rating": {
"current_interval_price_rating": {
"name": "Nuvarande prisvärdering",
"state": {
"low": "Låg",
@ -503,7 +503,7 @@
"very_high": "Mycket hög"
}
},
"price_level": {
"current_interval_price_level": {
"options": {
"any": "Alla",
"very_cheap": "Mycket billigt",