mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
feat: Add price trend thresholds configuration and update related calculations
This commit is contained in:
parent
5ba0633d15
commit
db0d65a939
9 changed files with 151 additions and 13 deletions
|
|
@ -48,6 +48,8 @@ from .const import (
|
|||
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
DEFAULT_BEST_PRICE_FLEX,
|
||||
DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||
DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
|
||||
|
|
@ -57,6 +59,8 @@ from .const import (
|
|||
DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
|
@ -590,7 +594,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
"""Configure peak price period settings."""
|
||||
if user_input is not None:
|
||||
self._options.update(user_input)
|
||||
return self.async_create_entry(title="", data=self._options)
|
||||
return await self.async_step_price_trend()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="peak_price",
|
||||
|
|
@ -648,3 +652,49 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_price_trend(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
"""Configure price trend thresholds."""
|
||||
if user_input is not None:
|
||||
self._options.update(user_input)
|
||||
return self.async_create_entry(title="", data=self._options)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="price_trend",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
default=int(
|
||||
self.config_entry.options.get(
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||
)
|
||||
),
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=1,
|
||||
max=50,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
default=int(
|
||||
self.config_entry.options.get(
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||
)
|
||||
),
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=-50,
|
||||
max=-1,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ CONF_BEST_PRICE_MIN_PERIOD_LENGTH = "best_price_min_period_length"
|
|||
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH = "peak_price_min_period_length"
|
||||
CONF_PRICE_RATING_THRESHOLD_LOW = "price_rating_threshold_low"
|
||||
CONF_PRICE_RATING_THRESHOLD_HIGH = "price_rating_threshold_high"
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING = "price_trend_threshold_rising"
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING = "price_trend_threshold_falling"
|
||||
|
||||
ATTRIBUTION = "Data provided by Tibber"
|
||||
|
||||
|
|
@ -40,6 +42,8 @@ DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH = 60 # 60 minutes minimum period length fo
|
|||
DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH = 60 # 60 minutes minimum period length for peak price (user-facing, minutes)
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW = -10 # Default rating threshold low percentage
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_HIGH = 10 # Default rating threshold high percentage
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING = 5 # Default trend threshold for rising prices (%)
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING = -5 # Default trend threshold for falling prices (%, negative value)
|
||||
|
||||
# Home types
|
||||
HOME_TYPE_APARTMENT = "APARTMENT"
|
||||
|
|
|
|||
|
|
@ -413,7 +413,8 @@ def aggregate_period_ratings(
|
|||
def calculate_price_trend(
|
||||
current_price: float,
|
||||
future_average: float,
|
||||
threshold_pct: float = 5.0,
|
||||
threshold_rising: float = 5.0,
|
||||
threshold_falling: float = -5.0,
|
||||
) -> tuple[str, float]:
|
||||
"""
|
||||
Calculate price trend by comparing current price with future average.
|
||||
|
|
@ -421,7 +422,8 @@ def calculate_price_trend(
|
|||
Args:
|
||||
current_price: Current interval price
|
||||
future_average: Average price of future intervals
|
||||
threshold_pct: Percentage threshold for stable vs rising/falling (default 5%)
|
||||
threshold_rising: Percentage threshold for rising trend (positive, default 5%)
|
||||
threshold_falling: Percentage threshold for falling trend (negative, default -5%)
|
||||
|
||||
Returns:
|
||||
Tuple of (trend_state, difference_percentage)
|
||||
|
|
@ -436,10 +438,11 @@ def calculate_price_trend(
|
|||
# Calculate percentage difference from current to future
|
||||
diff_pct = ((future_average - current_price) / current_price) * 100
|
||||
|
||||
# Determine trend based on threshold
|
||||
if diff_pct > threshold_pct:
|
||||
# Determine trend based on thresholds
|
||||
# threshold_falling is negative, so we compare with it directly
|
||||
if diff_pct > threshold_rising:
|
||||
trend = "rising"
|
||||
elif diff_pct < -threshold_pct:
|
||||
elif diff_pct < threshold_falling:
|
||||
trend = "falling"
|
||||
else:
|
||||
trend = "stable"
|
||||
|
|
|
|||
|
|
@ -29,9 +29,13 @@ from .const import (
|
|||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||
DOMAIN,
|
||||
PRICE_LEVEL_MAPPING,
|
||||
PRICE_RATING_MAPPING,
|
||||
|
|
@ -522,6 +526,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
self._value_getter: Callable | None = self._get_value_getter()
|
||||
self._time_sensitive_remove_listener: Callable | None = None
|
||||
self._trend_attributes: dict[str, Any] = {} # Sensor-specific trend attributes
|
||||
self._cached_trend_value: str | None = None # Cache for trend state
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
|
|
@ -545,8 +550,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
@callback
|
||||
def _handle_time_sensitive_update(self) -> None:
|
||||
"""Handle time-sensitive update from coordinator."""
|
||||
# Clear cached trend values on time-sensitive updates
|
||||
if self.entity_description.key.startswith("price_trend_"):
|
||||
self._cached_trend_value = None
|
||||
self._trend_attributes = {}
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
# Clear cached trend values when coordinator data changes
|
||||
if self.entity_description.key.startswith("price_trend_"):
|
||||
self._cached_trend_value = None
|
||||
self._trend_attributes = {}
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _get_value_getter(self) -> Callable | None:
|
||||
"""Return the appropriate value getter method based on the sensor type."""
|
||||
key = self.entity_description.key
|
||||
|
|
@ -1201,6 +1219,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
Trend state: "rising" | "falling" | "stable", or None if unavailable
|
||||
|
||||
"""
|
||||
# Return cached value if available to ensure consistency between
|
||||
# native_value and extra_state_attributes
|
||||
if self._cached_trend_value is not None and self._trend_attributes:
|
||||
return self._cached_trend_value
|
||||
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
|
|
@ -1223,15 +1246,29 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
if future_avg is None:
|
||||
return None
|
||||
|
||||
# Calculate trend with 5% threshold
|
||||
trend_state, diff_pct = calculate_price_trend(current_price, future_avg, threshold_pct=5.0)
|
||||
# Get configured thresholds from options
|
||||
threshold_rising = self.coordinator.config_entry.options.get(
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||
)
|
||||
threshold_falling = self.coordinator.config_entry.options.get(
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||
)
|
||||
|
||||
# Store attributes in sensor-specific dictionary (not shared _attr_extra_state_attributes)
|
||||
# Calculate trend with configured thresholds
|
||||
trend_state, diff_pct = calculate_price_trend(
|
||||
current_price, future_avg, threshold_rising=threshold_rising, threshold_falling=threshold_falling
|
||||
)
|
||||
|
||||
# Store attributes in sensor-specific dictionary AND cache the trend value
|
||||
self._trend_attributes = {
|
||||
"timestamp": next_interval_start.isoformat(),
|
||||
f"trend_{hours}h_%": round(diff_pct, 1),
|
||||
f"future_avg_{hours}h": round(future_avg * 100, 2),
|
||||
"intervals_analyzed": hours * 4,
|
||||
"interval_count": hours * 4,
|
||||
"threshold_rising": threshold_rising,
|
||||
"threshold_falling": threshold_falling,
|
||||
}
|
||||
|
||||
# Calculate additional attributes for better granularity
|
||||
|
|
@ -1243,8 +1280,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
|
||||
# Calculate incremental change: how much does the later half differ from current?
|
||||
if current_price > 0:
|
||||
incremental_diff = ((later_half_avg - current_price) / current_price) * 100
|
||||
self._trend_attributes["incremental_change"] = round(incremental_diff, 1)
|
||||
later_half_diff = ((later_half_avg - current_price) / current_price) * 100
|
||||
self._trend_attributes[f"later_half_diff_{hours}h_%"] = round(later_half_diff, 1)
|
||||
|
||||
# Cache the trend value for consistency
|
||||
self._cached_trend_value = trend_state
|
||||
|
||||
return trend_state
|
||||
|
||||
|
|
@ -1588,7 +1628,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
key = self.entity_description.key
|
||||
attributes = {}
|
||||
|
||||
# For trend sensors, merge _trend_attributes (sensor-specific)
|
||||
# For trend sensors, use the cached _trend_attributes
|
||||
# These are populated when native_value is calculated
|
||||
if key.startswith("price_trend_") and hasattr(self, "_trend_attributes") and self._trend_attributes:
|
||||
attributes.update(self._trend_attributes)
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@
|
|||
"peak_price_flex": "Flexibilität: Maximale % unter dem Höchstpreis (negativer Wert)",
|
||||
"peak_price_min_distance_from_avg": "Mindestabstand: Erforderliche % über dem Tagesdurchschnitt"
|
||||
}
|
||||
},
|
||||
"price_trend": {
|
||||
"title": "Preistrend-Schwellenwerte",
|
||||
"description": "Konfiguration der Schwellenwerte für Preistrend-Sensoren. Diese Sensoren vergleichen den aktuellen Preis mit dem Durchschnitt der nächsten N Stunden, um festzustellen, ob die Preise steigen, fallen oder stabil sind.",
|
||||
"data": {
|
||||
"price_trend_threshold_rising": "Schwellenwert für Steigen (% über aktuellem Preis)",
|
||||
"price_trend_threshold_falling": "Schwellenwert für Fallen (% unter aktuellem Preis, negativer Wert)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@
|
|||
"peak_price_flex": "Flexibility: Maximum % below maximum price (negative value)",
|
||||
"peak_price_min_distance_from_avg": "Minimum Distance: Required % above daily average"
|
||||
}
|
||||
},
|
||||
"price_trend": {
|
||||
"title": "Price Trend Thresholds",
|
||||
"description": "Configure thresholds for price trend sensors. These sensors compare the current price with the average of the next N hours to determine if prices are rising, falling, or stable.",
|
||||
"data": {
|
||||
"price_trend_threshold_rising": "Rising Threshold (% above current price)",
|
||||
"price_trend_threshold_falling": "Falling Threshold (% below current price, negative value)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@
|
|||
"peak_price_flex": "Fleksibilitet: Maksimum % under maksimumspris (negativ verdi)",
|
||||
"peak_price_min_distance_from_avg": "Minimumsavstand: Påkrevd % over daglig gjennomsnitt"
|
||||
}
|
||||
},
|
||||
"price_trend": {
|
||||
"title": "Terskelverdier for pristrend",
|
||||
"description": "Konfigurer terskelverdier for pristrendsensorer. Disse sensorene sammenligner gjeldende pris med gjennomsnittet av de neste N timene for å avgjøre om prisene stiger, faller eller er stabile.",
|
||||
"data": {
|
||||
"price_trend_threshold_rising": "Stigende terskelverdi (% over gjeldende pris)",
|
||||
"price_trend_threshold_falling": "Fallende terskelverdi (% under gjeldende pris, negativ verdi)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@
|
|||
"peak_price_flex": "Flexibiliteit: Maximaal % onder maximumprijs (negatieve waarde)",
|
||||
"peak_price_min_distance_from_avg": "Minimale afstand: Vereist % boven dagelijks gemiddelde"
|
||||
}
|
||||
},
|
||||
"price_trend": {
|
||||
"title": "Prijstrend drempelwaarden",
|
||||
"description": "Configureer drempelwaarden voor prijstrendsensoren. Deze sensoren vergelijken de huidige prijs met het gemiddelde van de volgende N uur om te bepalen of de prijzen stijgen, dalen of stabiel zijn.",
|
||||
"data": {
|
||||
"price_trend_threshold_rising": "Stijgende drempelwaarde (% boven huidige prijs)",
|
||||
"price_trend_threshold_falling": "Dalende drempelwaarde (% onder huidige prijs, negatieve waarde)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@
|
|||
"peak_price_flex": "Flexibilitet: Maximalt % under maximumpris (negativt värde)",
|
||||
"peak_price_min_distance_from_avg": "Minimiavstånd: Krävd % över dagligt genomsnitt"
|
||||
}
|
||||
},
|
||||
"price_trend": {
|
||||
"title": "Pristrend tröskelvärden",
|
||||
"description": "Konfigurera tröskelvärden för pristrendsensorer. Dessa sensorer jämför det aktuella priset med genomsnittet av de nästa N timmarna för att avgöra om priserna stiger, faller eller är stabila.",
|
||||
"data": {
|
||||
"price_trend_threshold_rising": "Stigande tröskelvärde (% över aktuellt pris)",
|
||||
"price_trend_threshold_falling": "Fallande tröskelvärde (% under aktuellt pris, negativt värde)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue