mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
feat(sensors)!: rename price_trend_Xh → price_outlook_Xh, add price_trajectory_Xh
Renamed 8 sensors to clarify what they actually measure, and added 7 new
sensors for a different (and often more useful) calculation.
--- WHY THE RENAME ---
The old name "price_trend_Xh" implied the sensor shows where prices are
heading. It doesn't — it compares CURRENT price vs the FUTURE WINDOW AVERAGE.
At a price minimum, it shows "strongly_falling" (because the cheap minimum
pulls the average below your current high price), which is the opposite of
intuitive. The name "price_outlook_Xh" correctly conveys: "is now cheaper
or more expensive than the next Nh on average?"
--- NEW: price_trajectory_Xh ---
These sensors compare FIRST HALF vs SECOND HALF of the window, revealing
actual price direction within the window:
price_trajectory_2h: avg(hour 1) vs avg(hour 2)
price_trajectory_3h: avg(first 1.5h) vs avg(second 1.5h)
price_trajectory_4h: avg(first 2h) vs avg(second 2h)
price_trajectory_5h: avg(first 2.5h) vs avg(second 2.5h)
price_trajectory_6h: avg(first 3h) vs avg(second 3h)
price_trajectory_8h: avg(first 4h) vs avg(second 4h)
price_trajectory_12h: avg(first 6h) vs avg(second 6h)
The key use case: at a price minimum, price_outlook_Xh shows "strongly_falling"
but price_trajectory_Xh shows "rising" — correctly revealing the upcoming
reversal. "outlook: falling + trajectory: rising" = you're AT the minimum.
--- IMPLEMENTATION ---
sensor/calculators/trend.py:
- get_price_outlook_value() (was: get_price_trend_value())
- New: get_price_trajectory_value(*, hours: int)
- New: _calculate_first_half_average(hours, next_interval_start)
- New: get_trajectory_attributes() → first_half_avg, second_half_avg, half_diff_%
- clear_trend_cache() also resets _trajectory_attributes
sensor/definitions.py:
- 8 SensorEntityDescription entries: key/translation_key price_trend_Xh → price_outlook_Xh
- New PRICE_TRAJECTORY_SENSORS tuple (2h–5h enabled by default, 6h/8h/12h disabled)
sensor/value_getters.py:
- 8 lambda entries renamed
- 7 new trajectory lambda entries added
sensor/attributes/trend.py:
- startswith("price_trend_") → startswith("price_outlook_")
- New elif branch routing price_trajectory_* to cached trajectory_attributes
sensor/core.py:
- startswith checks updated for both prefix families
- cached_data dict extended with "trajectory_attributes"
coordinator/constants.py:
- TIME_SENSITIVE_ENTITY_KEYS: 8 renamed + 7 new trajectory keys added
config_flow_handlers/entity_check.py:
- volatility + price_trend affected-entity lists: 8 renamed + 7 new
BREAKING CHANGE: Sensors price_trend_1h, price_trend_2h, price_trend_3h,
price_trend_4h, price_trend_5h, price_trend_6h, price_trend_8h,
price_trend_12h have been removed without a deprecation period.
Migration:
Replace price_trend_Xh → price_outlook_Xh everywhere (automations,
dashboards, templates). Behavior is identical — only the entity name
changed. If you want to detect actual price direction within the window
(e.g. "are prices rising or falling right now?"), use the new
price_trajectory_Xh sensors instead.
Impact: Users must update automations and dashboards. Entity IDs change from
sensor.<home>_price_trend_Xh to sensor.<home>_price_outlook_Xh. New
price_trajectory_Xh sensors provide complementary direction information.
This commit is contained in:
parent
d0b6ea0e1a
commit
33f57ff077
7 changed files with 354 additions and 65 deletions
|
|
@ -69,14 +69,21 @@ STEP_TO_SENSOR_KEYS: dict[str, list[str]] = {
|
|||
# Also affects trend sensors (adaptive thresholds)
|
||||
"current_price_trend",
|
||||
"next_price_trend_change",
|
||||
"price_trend_1h",
|
||||
"price_trend_2h",
|
||||
"price_trend_3h",
|
||||
"price_trend_4h",
|
||||
"price_trend_5h",
|
||||
"price_trend_6h",
|
||||
"price_trend_8h",
|
||||
"price_trend_12h",
|
||||
"price_outlook_1h",
|
||||
"price_outlook_2h",
|
||||
"price_outlook_3h",
|
||||
"price_outlook_4h",
|
||||
"price_outlook_5h",
|
||||
"price_outlook_6h",
|
||||
"price_outlook_8h",
|
||||
"price_outlook_12h",
|
||||
"price_trajectory_2h",
|
||||
"price_trajectory_3h",
|
||||
"price_trajectory_4h",
|
||||
"price_trajectory_5h",
|
||||
"price_trajectory_6h",
|
||||
"price_trajectory_8h",
|
||||
"price_trajectory_12h",
|
||||
],
|
||||
# Best Price settings affect best price binary sensor and timing sensors
|
||||
"best_price": [
|
||||
|
|
@ -106,14 +113,21 @@ STEP_TO_SENSOR_KEYS: dict[str, list[str]] = {
|
|||
"price_trend": [
|
||||
"current_price_trend",
|
||||
"next_price_trend_change",
|
||||
"price_trend_1h",
|
||||
"price_trend_2h",
|
||||
"price_trend_3h",
|
||||
"price_trend_4h",
|
||||
"price_trend_5h",
|
||||
"price_trend_6h",
|
||||
"price_trend_8h",
|
||||
"price_trend_12h",
|
||||
"price_outlook_1h",
|
||||
"price_outlook_2h",
|
||||
"price_outlook_3h",
|
||||
"price_outlook_4h",
|
||||
"price_outlook_5h",
|
||||
"price_outlook_6h",
|
||||
"price_outlook_8h",
|
||||
"price_outlook_12h",
|
||||
"price_trajectory_2h",
|
||||
"price_trajectory_3h",
|
||||
"price_trajectory_4h",
|
||||
"price_trajectory_5h",
|
||||
"price_trajectory_6h",
|
||||
"price_trajectory_8h",
|
||||
"price_trajectory_12h",
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,14 +62,22 @@ TIME_SENSITIVE_ENTITY_KEYS = frozenset(
|
|||
"current_price_trend",
|
||||
"next_price_trend_change",
|
||||
# Price trend sensors
|
||||
"price_trend_1h",
|
||||
"price_trend_2h",
|
||||
"price_trend_3h",
|
||||
"price_trend_4h",
|
||||
"price_trend_5h",
|
||||
"price_trend_6h",
|
||||
"price_trend_8h",
|
||||
"price_trend_12h",
|
||||
"price_outlook_1h",
|
||||
"price_outlook_2h",
|
||||
"price_outlook_3h",
|
||||
"price_outlook_4h",
|
||||
"price_outlook_5h",
|
||||
"price_outlook_6h",
|
||||
"price_outlook_8h",
|
||||
"price_outlook_12h",
|
||||
# Price trajectory sensors (first-half vs second-half window comparison)
|
||||
"price_trajectory_2h",
|
||||
"price_trajectory_3h",
|
||||
"price_trajectory_4h",
|
||||
"price_trajectory_5h",
|
||||
"price_trajectory_6h",
|
||||
"price_trajectory_8h",
|
||||
"price_trajectory_12h",
|
||||
# Trailing/leading 24h calculations (based on current interval)
|
||||
"trailing_price_average",
|
||||
"leading_price_average",
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ def _add_timing_or_volatility_attributes(
|
|||
|
||||
def _add_cached_trend_attributes(attributes: dict, key: str, cached_data: dict) -> None:
|
||||
"""Add cached trend attributes if available."""
|
||||
if key.startswith("price_trend_") and cached_data.get("trend_attributes"):
|
||||
if key.startswith("price_outlook_") and cached_data.get("trend_attributes"):
|
||||
attributes.update(cached_data["trend_attributes"])
|
||||
elif key.startswith("price_trajectory_") and cached_data.get("trajectory_attributes"):
|
||||
attributes.update(cached_data["trajectory_attributes"])
|
||||
elif key == "current_price_trend" and cached_data.get("current_trend_attributes"):
|
||||
# Add cached attributes (timestamp already set by platform)
|
||||
attributes.update(cached_data["current_trend_attributes"])
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
Trend calculator for price trend analysis sensors.
|
||||
|
||||
This module handles all trend-related calculations:
|
||||
- Simple price trends (1h-12h future comparison)
|
||||
- Price outlook (1h-12h): Current price vs average of the next N hours
|
||||
- Price trajectory (2h-12h): First-half vs second-half average in the window (shows turning points)
|
||||
- Current trend (pure future-based 3h outlook with volatility adjustment)
|
||||
- Next trend change prediction (with configurable N-interval hysteresis, default 3)
|
||||
- Trend duration tracking (lightweight price direction scan with noise tolerance)
|
||||
|
||||
Caching strategy:
|
||||
- Simple trends: Cached per sensor update to ensure consistency between state and attributes
|
||||
- Outlook/Trajectory: Cached per sensor update to ensure consistency between state and attributes
|
||||
- Current trend + next change: Cached centrally for 60s to avoid duplicate calculations
|
||||
"""
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ if TYPE_CHECKING:
|
|||
)
|
||||
|
||||
# Constants
|
||||
MIN_HOURS_FOR_LATER_HALF = 3 # Minimum hours needed to calculate later half average
|
||||
MIN_HOURS_FOR_LATER_HALF = 1 # Minimum hours needed to calculate half-window averages (activates at 2h+)
|
||||
|
||||
|
||||
class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
||||
|
|
@ -39,9 +40,10 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
Calculator for price trend sensors.
|
||||
|
||||
Handles three types of trend analysis:
|
||||
1. Simple trends (price_trend_1h-12h): Current vs next N hours average
|
||||
2. Current trend (current_price_trend): Pure future-based 3h outlook with volatility adjustment
|
||||
3. Next change (next_price_trend_change): Scan forward with configurable N-interval hysteresis (default 3)
|
||||
1. Outlook sensors (price_outlook_1h-12h): Current vs next N hours average
|
||||
2. Trajectory sensors (price_trajectory_2h-12h): First half vs second half of window
|
||||
3. Current trend (current_price_trend): Pure future-based 3h outlook with volatility adjustment
|
||||
4. Next change (next_price_trend_change): Scan forward with configurable N-interval hysteresis (default 3)
|
||||
|
||||
Caching:
|
||||
- Simple trends: Per-sensor cache (_cached_trend_value, _trend_attributes)
|
||||
|
|
@ -62,9 +64,10 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
def __init__(self, coordinator: "TibberPricesDataUpdateCoordinator") -> None:
|
||||
"""Initialize trend calculator with caching state."""
|
||||
super().__init__(coordinator)
|
||||
# Per-sensor trend caches (for price_trend_Nh sensors)
|
||||
# Per-sensor caches (for price_outlook_Xh and price_trajectory_Xh sensors)
|
||||
self._cached_trend_value: str | None = None
|
||||
self._trend_attributes: dict[str, Any] = {}
|
||||
self._trajectory_attributes: dict[str, Any] = {}
|
||||
# Centralized trend calculation cache (for current_price_trend + next_price_trend_change)
|
||||
self._trend_calculation_cache: dict[str, Any] | None = None
|
||||
self._trend_calculation_timestamp: datetime | None = None
|
||||
|
|
@ -72,11 +75,12 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
self._current_trend_attributes: dict[str, Any] | None = None
|
||||
self._trend_change_attributes: dict[str, Any] | None = None
|
||||
|
||||
def get_price_trend_value(self, *, hours: int) -> str | None:
|
||||
def get_price_outlook_value(self, *, hours: int) -> str | None:
|
||||
"""
|
||||
Calculate price trend comparing current interval vs next N hours average.
|
||||
Calculate price outlook comparing current interval vs average of the next N hours.
|
||||
|
||||
This is for simple trend sensors (price_trend_1h through price_trend_12h).
|
||||
This is for price_outlook_Xh sensors. Answers: "Is the average of the next Xh
|
||||
cheaper or more expensive than right now?"
|
||||
Results are cached per sensor to ensure consistency between state and attributes.
|
||||
|
||||
Args:
|
||||
|
|
@ -284,10 +288,131 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
|
||||
return trend_info["minutes_until_change"]
|
||||
|
||||
def get_price_trajectory_value(self, *, hours: int) -> str | None:
|
||||
"""
|
||||
Calculate price trajectory by comparing first-half vs second-half window average.
|
||||
|
||||
This is for price_trajectory_Xh sensors. Answers: "Are prices rising or falling
|
||||
within the next Xh window?" — revealing turning points that price_outlook_Xh misses.
|
||||
|
||||
Example at a price minimum (12:00):
|
||||
- price_outlook_4h: "strongly_falling" (Ø next 4h is below current high)
|
||||
- price_trajectory_4h: "rising" (second half is more expensive than first half)
|
||||
→ Combined signal: act now, reversal is coming within the window.
|
||||
|
||||
Args:
|
||||
hours: Number of hours in the window (must be >= 2)
|
||||
|
||||
Returns:
|
||||
Trend state: "rising" | "falling" | "stable", or None if unavailable
|
||||
|
||||
"""
|
||||
if hours < 2: # noqa: PLR2004
|
||||
return None
|
||||
|
||||
if not self.has_data():
|
||||
return None
|
||||
|
||||
current_interval = self.coordinator.get_current_interval()
|
||||
if not current_interval or "total" not in current_interval:
|
||||
return None
|
||||
|
||||
current_interval_price = float(current_interval["total"])
|
||||
time = self.coordinator.time
|
||||
current_starts_at = time.get_interval_time(current_interval)
|
||||
if current_starts_at is None:
|
||||
return None
|
||||
|
||||
next_interval_start = time.get_next_interval_start()
|
||||
|
||||
# Get first-half and second-half averages
|
||||
first_half_avg = self._calculate_first_half_average(hours, next_interval_start)
|
||||
second_half_avg = self._calculate_later_half_average(hours, next_interval_start)
|
||||
|
||||
if first_half_avg is None or second_half_avg is None:
|
||||
return None
|
||||
|
||||
# Get configured thresholds (same as outlook sensors for consistency)
|
||||
threshold_rising = self.config.get("price_trend_threshold_rising", 3.0)
|
||||
threshold_falling = self.config.get("price_trend_threshold_falling", -3.0)
|
||||
threshold_strongly_rising = self.config.get("price_trend_threshold_strongly_rising", 9.0)
|
||||
threshold_strongly_falling = self.config.get("price_trend_threshold_strongly_falling", -9.0)
|
||||
volatility_threshold_moderate = self.config.get("volatility_threshold_moderate", 15.0)
|
||||
volatility_threshold_high = self.config.get("volatility_threshold_high", 30.0)
|
||||
min_abs_diff = self.config.get("price_trend_min_price_change", 0.005)
|
||||
min_abs_diff_strongly = self.config.get("price_trend_min_price_change_strongly", 0.015)
|
||||
|
||||
# Build volatility window from full outlook period
|
||||
today_prices = self.intervals_today
|
||||
tomorrow_prices = self.intervals_tomorrow
|
||||
all_intervals = today_prices + tomorrow_prices
|
||||
lookahead_intervals = self.coordinator.time.minutes_to_intervals(hours * 60)
|
||||
|
||||
current_idx = None
|
||||
for idx, interval in enumerate(all_intervals):
|
||||
if time.get_interval_time(interval) == current_starts_at:
|
||||
current_idx = idx
|
||||
break
|
||||
|
||||
if current_idx is not None:
|
||||
volatility_window = all_intervals[current_idx : current_idx + lookahead_intervals]
|
||||
else:
|
||||
volatility_window = all_intervals[:lookahead_intervals]
|
||||
|
||||
# Compare first half vs second half: does price rise or fall across the window?
|
||||
trajectory_state, diff_pct, trend_value, vol_factor = calculate_price_trend(
|
||||
first_half_avg,
|
||||
second_half_avg,
|
||||
threshold_rising=threshold_rising,
|
||||
threshold_falling=threshold_falling,
|
||||
threshold_strongly_rising=threshold_strongly_rising,
|
||||
threshold_strongly_falling=threshold_strongly_falling,
|
||||
min_abs_diff=min_abs_diff,
|
||||
min_abs_diff_strongly=min_abs_diff_strongly,
|
||||
volatility_adjustment=True,
|
||||
lookahead_intervals=lookahead_intervals,
|
||||
all_intervals=volatility_window,
|
||||
volatility_threshold_moderate=volatility_threshold_moderate,
|
||||
volatility_threshold_high=volatility_threshold_high,
|
||||
)
|
||||
|
||||
factor = get_display_unit_factor(self.config_entry)
|
||||
time_obj = self.coordinator.time
|
||||
total_intervals = time_obj.minutes_to_intervals(hours * 60)
|
||||
first_half_count = total_intervals // 2
|
||||
second_half_count = total_intervals - first_half_count
|
||||
|
||||
self._trajectory_attributes = {
|
||||
"timestamp": next_interval_start,
|
||||
"trend_value": trend_value,
|
||||
f"trajectory_{hours}h_%": round(diff_pct, 1),
|
||||
f"first_half_{hours}h_avg": round(first_half_avg * factor, 2),
|
||||
f"second_half_{hours}h_avg": round(second_half_avg * factor, 2),
|
||||
f"first_half_{hours}h_diff_from_current_%": round(
|
||||
((first_half_avg - current_interval_price) / abs(current_interval_price)) * 100, 1
|
||||
)
|
||||
if current_interval_price != 0
|
||||
else None,
|
||||
f"second_half_{hours}h_diff_from_current_%": round(
|
||||
((second_half_avg - current_interval_price) / abs(current_interval_price)) * 100, 1
|
||||
)
|
||||
if current_interval_price != 0
|
||||
else None,
|
||||
"first_half_interval_count": first_half_count,
|
||||
"second_half_interval_count": second_half_count,
|
||||
"volatility_factor": vol_factor,
|
||||
}
|
||||
|
||||
return trajectory_state
|
||||
|
||||
def get_trend_attributes(self) -> dict[str, Any]:
|
||||
"""Get cached trend attributes for simple trend sensors (price_trend_Nh)."""
|
||||
"""Get cached outlook attributes for price_outlook_Xh sensors."""
|
||||
return self._trend_attributes
|
||||
|
||||
def get_trajectory_attributes(self) -> dict[str, Any]:
|
||||
"""Get cached trajectory attributes for price_trajectory_Xh sensors."""
|
||||
return self._trajectory_attributes
|
||||
|
||||
def get_current_trend_attributes(self) -> dict[str, Any] | None:
|
||||
"""Get cached attributes for current_price_trend sensor."""
|
||||
return self._current_trend_attributes
|
||||
|
|
@ -297,9 +422,10 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
return self._trend_change_attributes
|
||||
|
||||
def clear_trend_cache(self) -> None:
|
||||
"""Clear simple trend cache (called on coordinator update)."""
|
||||
"""Clear outlook/trajectory trend cache (called on coordinator update)."""
|
||||
self._cached_trend_value = None
|
||||
self._trend_attributes = {}
|
||||
self._trajectory_attributes = {}
|
||||
|
||||
def clear_calculation_cache(self) -> None:
|
||||
"""Clear centralized trend calculation cache (called on coordinator update)."""
|
||||
|
|
@ -310,6 +436,54 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
# PRIVATE HELPER METHODS
|
||||
# ========================================================================
|
||||
|
||||
def _calculate_first_half_average(self, hours: int, next_interval_start: datetime) -> float | None:
|
||||
"""
|
||||
Calculate average price for the first half of the future time window.
|
||||
|
||||
This is the counterpart to _calculate_later_half_average and together they
|
||||
enable trajectory calculation (first half vs second half comparison).
|
||||
|
||||
Args:
|
||||
hours: Total hours in the prediction window
|
||||
next_interval_start: Start timestamp of the next interval
|
||||
|
||||
Returns:
|
||||
Average price for the first half intervals, or None if insufficient data
|
||||
|
||||
"""
|
||||
if not self.has_data():
|
||||
return None
|
||||
|
||||
today_prices = self.intervals_today
|
||||
tomorrow_prices = self.intervals_tomorrow
|
||||
all_prices = today_prices + tomorrow_prices
|
||||
|
||||
if not all_prices:
|
||||
return None
|
||||
|
||||
time = self.coordinator.time
|
||||
total_intervals = time.minutes_to_intervals(hours * 60)
|
||||
first_half_intervals = total_intervals // 2
|
||||
interval_duration = time.get_interval_duration()
|
||||
first_half_end = next_interval_start + (interval_duration * first_half_intervals)
|
||||
|
||||
# Collect prices in the first half: [next_interval_start, first_half_end)
|
||||
first_prices = []
|
||||
for price_data in all_prices:
|
||||
starts_at = time.get_interval_time(price_data)
|
||||
if starts_at is None:
|
||||
continue
|
||||
|
||||
if next_interval_start <= starts_at < first_half_end:
|
||||
price = price_data.get("total")
|
||||
if price is not None:
|
||||
first_prices.append(float(price))
|
||||
|
||||
if first_prices:
|
||||
return calculate_mean(first_prices)
|
||||
|
||||
return None
|
||||
|
||||
def _calculate_later_half_average(self, hours: int, next_interval_start: datetime) -> float | None:
|
||||
"""
|
||||
Calculate average price for the later half of the future time window.
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
self.coordinator.time = time_service
|
||||
|
||||
# Clear cached trend values on time-sensitive updates
|
||||
if self.entity_description.key.startswith("price_trend_"):
|
||||
if self.entity_description.key.startswith(("price_outlook_", "price_trajectory_")):
|
||||
self._trend_calculator.clear_trend_cache()
|
||||
# Clear trend calculation cache for trend sensors
|
||||
elif self.entity_description.key in (
|
||||
|
|
@ -366,7 +366,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
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_"):
|
||||
if self.entity_description.key.startswith(("price_outlook_", "price_trajectory_")):
|
||||
self._trend_calculator.clear_trend_cache()
|
||||
# Also clear calculation cache (e.g., when threshold config changes)
|
||||
self._trend_calculator.clear_calculation_cache()
|
||||
|
|
@ -1140,6 +1140,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
cached_data.update(
|
||||
{
|
||||
"trend_attributes": self._trend_calculator.get_trend_attributes(),
|
||||
"trajectory_attributes": self._trend_calculator.get_trajectory_attributes(),
|
||||
"current_trend_attributes": self._trend_calculator.get_current_trend_attributes(),
|
||||
"trend_change_attributes": self._trend_calculator.get_trend_change_attributes(),
|
||||
"volatility_attributes": self._volatility_calculator.get_volatility_attributes(),
|
||||
|
|
|
|||
|
|
@ -529,11 +529,11 @@ FUTURE_TREND_SENSORS = (
|
|||
suggested_display_precision=2,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
# Price trend forecast sensors (will prices be higher/lower in X hours?)
|
||||
# Price outlook forecast sensors (is the average of the next Xh cheaper/more expensive than now?)
|
||||
# Default enabled: 1h-5h
|
||||
SensorEntityDescription(
|
||||
key="price_trend_1h",
|
||||
translation_key="price_trend_1h",
|
||||
key="price_outlook_1h",
|
||||
translation_key="price_outlook_1h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
|
|
@ -541,8 +541,8 @@ FUTURE_TREND_SENSORS = (
|
|||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trend_2h",
|
||||
translation_key="price_trend_2h",
|
||||
key="price_outlook_2h",
|
||||
translation_key="price_outlook_2h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
|
|
@ -550,8 +550,8 @@ FUTURE_TREND_SENSORS = (
|
|||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trend_3h",
|
||||
translation_key="price_trend_3h",
|
||||
key="price_outlook_3h",
|
||||
translation_key="price_outlook_3h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
|
|
@ -559,8 +559,8 @@ FUTURE_TREND_SENSORS = (
|
|||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trend_4h",
|
||||
translation_key="price_trend_4h",
|
||||
key="price_outlook_4h",
|
||||
translation_key="price_outlook_4h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
|
|
@ -568,8 +568,8 @@ FUTURE_TREND_SENSORS = (
|
|||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trend_5h",
|
||||
translation_key="price_trend_5h",
|
||||
key="price_outlook_5h",
|
||||
translation_key="price_outlook_5h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
|
|
@ -578,8 +578,8 @@ FUTURE_TREND_SENSORS = (
|
|||
),
|
||||
# Disabled by default: 6h, 8h, 12h
|
||||
SensorEntityDescription(
|
||||
key="price_trend_6h",
|
||||
translation_key="price_trend_6h",
|
||||
key="price_outlook_6h",
|
||||
translation_key="price_outlook_6h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
|
|
@ -587,8 +587,8 @@ FUTURE_TREND_SENSORS = (
|
|||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trend_8h",
|
||||
translation_key="price_trend_8h",
|
||||
key="price_outlook_8h",
|
||||
translation_key="price_outlook_8h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
|
|
@ -596,8 +596,89 @@ FUTURE_TREND_SENSORS = (
|
|||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trend_12h",
|
||||
translation_key="price_trend_12h",
|
||||
key="price_outlook_12h",
|
||||
translation_key="price_outlook_12h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 5b. PRICE TRAJECTORY SENSORS (first-half vs second-half window comparison)
|
||||
# ----------------------------------------------------------------------------
|
||||
# These sensors reveal turning points: is the price rising or falling WITHIN
|
||||
# the window? Complements price_outlook_Xh sensors.
|
||||
#
|
||||
# Example at a price minimum (12:00):
|
||||
# - price_outlook_4h: "strongly_falling" (Ø next 4h is below current high)
|
||||
# - price_trajectory_4h: "rising" (second half avg > first half avg)
|
||||
# → Combined: act now, reversal is coming within the window.
|
||||
#
|
||||
# Coverage starts at 2h (minimum for meaningful first/second half split).
|
||||
# Default enabled: 2h-5h
|
||||
|
||||
PRICE_TRAJECTORY_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
key="price_trajectory_2h",
|
||||
translation_key="price_trajectory_2h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trajectory_3h",
|
||||
translation_key="price_trajectory_3h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trajectory_4h",
|
||||
translation_key="price_trajectory_4h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trajectory_5h",
|
||||
translation_key="price_trajectory_5h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
# Disabled by default: 6h, 8h, 12h
|
||||
SensorEntityDescription(
|
||||
key="price_trajectory_6h",
|
||||
translation_key="price_trajectory_6h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trajectory_8h",
|
||||
translation_key="price_trajectory_8h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_trajectory_12h",
|
||||
translation_key="price_trajectory_12h",
|
||||
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
|
|
@ -970,6 +1051,7 @@ ENTITY_DESCRIPTIONS = (
|
|||
*WINDOW_24H_SENSORS,
|
||||
*FUTURE_MEAN_SENSORS,
|
||||
*FUTURE_TREND_SENSORS,
|
||||
*PRICE_TRAJECTORY_SENSORS,
|
||||
*VOLATILITY_SENSORS,
|
||||
*BEST_PRICE_TIMING_SENSORS,
|
||||
*PEAK_PRICE_TIMING_SENSORS,
|
||||
|
|
|
|||
|
|
@ -206,15 +206,23 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
|
|||
"current_price_trend": trend_calculator.get_current_trend_value,
|
||||
"next_price_trend_change": trend_calculator.get_next_trend_change_value,
|
||||
"trend_change_in_minutes": lambda: _minutes_to_hours(trend_calculator.get_trend_change_in_minutes_value()),
|
||||
# Price trend sensors
|
||||
"price_trend_1h": lambda: trend_calculator.get_price_trend_value(hours=1),
|
||||
"price_trend_2h": lambda: trend_calculator.get_price_trend_value(hours=2),
|
||||
"price_trend_3h": lambda: trend_calculator.get_price_trend_value(hours=3),
|
||||
"price_trend_4h": lambda: trend_calculator.get_price_trend_value(hours=4),
|
||||
"price_trend_5h": lambda: trend_calculator.get_price_trend_value(hours=5),
|
||||
"price_trend_6h": lambda: trend_calculator.get_price_trend_value(hours=6),
|
||||
"price_trend_8h": lambda: trend_calculator.get_price_trend_value(hours=8),
|
||||
"price_trend_12h": lambda: trend_calculator.get_price_trend_value(hours=12),
|
||||
# Price outlook sensors (current price vs average of next Xh)
|
||||
"price_outlook_1h": lambda: trend_calculator.get_price_outlook_value(hours=1),
|
||||
"price_outlook_2h": lambda: trend_calculator.get_price_outlook_value(hours=2),
|
||||
"price_outlook_3h": lambda: trend_calculator.get_price_outlook_value(hours=3),
|
||||
"price_outlook_4h": lambda: trend_calculator.get_price_outlook_value(hours=4),
|
||||
"price_outlook_5h": lambda: trend_calculator.get_price_outlook_value(hours=5),
|
||||
"price_outlook_6h": lambda: trend_calculator.get_price_outlook_value(hours=6),
|
||||
"price_outlook_8h": lambda: trend_calculator.get_price_outlook_value(hours=8),
|
||||
"price_outlook_12h": lambda: trend_calculator.get_price_outlook_value(hours=12),
|
||||
# Price trajectory sensors (first-half vs second-half window, reveals turning points)
|
||||
"price_trajectory_2h": lambda: trend_calculator.get_price_trajectory_value(hours=2),
|
||||
"price_trajectory_3h": lambda: trend_calculator.get_price_trajectory_value(hours=3),
|
||||
"price_trajectory_4h": lambda: trend_calculator.get_price_trajectory_value(hours=4),
|
||||
"price_trajectory_5h": lambda: trend_calculator.get_price_trajectory_value(hours=5),
|
||||
"price_trajectory_6h": lambda: trend_calculator.get_price_trajectory_value(hours=6),
|
||||
"price_trajectory_8h": lambda: trend_calculator.get_price_trajectory_value(hours=8),
|
||||
"price_trajectory_12h": lambda: trend_calculator.get_price_trajectory_value(hours=12),
|
||||
# Diagnostic sensors
|
||||
"data_timestamp": get_data_timestamp,
|
||||
# Data lifecycle status sensor
|
||||
|
|
|
|||
Loading…
Reference in a new issue