feat(sensors): add 5-level price trend scale with configurable thresholds

Extends trend sensors from 3-level (rising/stable/falling) to 5-level scale
(strongly_rising/rising/stable/falling/strongly_falling) for finer granularity.

Changes:
- Add PRICE_TREND_MAPPING with integer values (-2, -1, 0, +1, +2) matching
  PRICE_LEVEL_MAPPING pattern for consistent automation comparisons
- Add configurable thresholds for strongly_rising (default: 6%) and
  strongly_falling (default: -6%) independent from base thresholds
- Update calculate_price_trend() to return 3-tuple: (trend_state, diff_pct, trend_value)
- Add trend_value attribute to all trend sensors for numeric comparisons
- Update sensor entity descriptions with 5-level options
- Add validation with cross-checks (strongly_rising > rising, etc.)
- Update icons: chevron-double-up/down for strong trends, trending-up/down for normal

Files changed:
- const.py: PRICE_TREND_* constants, PRICE_TREND_MAPPING, config constants
- utils/price.py: Extended calculate_price_trend() signature and return value
- sensor/calculators/trend.py: Pass new thresholds, handle 3-tuple return
- sensor/definitions.py: 5-level options for all 9 trend sensors
- sensor/core.py: 5-level icon mapping
- entity_utils/icons.py: 5-level trend icons
- config_flow_handlers/: validators, schemas, options_flow for new settings
- translations/*.json: Labels and error messages (en, de, nb, sv, nl)
- tests/test_percentage_calculations.py: Updated for 3-tuple return

Impact: Users get more nuanced trend information for automation decisions.
New trend_value attribute enables numeric comparisons (e.g., > 0 for any rise).
Existing automations using "rising"/"falling"/"stable" continue to work.
This commit is contained in:
Julian Pawlowski 2026-01-20 13:36:01 +00:00
parent 972cbce1d3
commit 5fc1f4db33
15 changed files with 510 additions and 137 deletions

View file

@ -33,6 +33,8 @@ from custom_components.tibber_prices.config_flow_handlers.validators import (
validate_price_rating_thresholds, validate_price_rating_thresholds,
validate_price_trend_falling, validate_price_trend_falling,
validate_price_trend_rising, validate_price_trend_rising,
validate_price_trend_strongly_falling,
validate_price_trend_strongly_rising,
validate_relaxation_attempts, validate_relaxation_attempts,
validate_volatility_threshold_high, validate_volatility_threshold_high,
validate_volatility_threshold_moderate, validate_volatility_threshold_moderate,
@ -54,6 +56,8 @@ from custom_components.tibber_prices.const import (
CONF_PRICE_RATING_THRESHOLD_LOW, CONF_PRICE_RATING_THRESHOLD_LOW,
CONF_PRICE_TREND_THRESHOLD_FALLING, CONF_PRICE_TREND_THRESHOLD_FALLING,
CONF_PRICE_TREND_THRESHOLD_RISING, CONF_PRICE_TREND_THRESHOLD_RISING,
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
CONF_RELAXATION_ATTEMPTS_BEST, CONF_RELAXATION_ATTEMPTS_BEST,
CONF_RELAXATION_ATTEMPTS_PEAK, CONF_RELAXATION_ATTEMPTS_PEAK,
CONF_VOLATILITY_THRESHOLD_HIGH, CONF_VOLATILITY_THRESHOLD_HIGH,
@ -493,6 +497,34 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
): ):
errors[CONF_PRICE_TREND_THRESHOLD_FALLING] = "invalid_price_trend_falling" errors[CONF_PRICE_TREND_THRESHOLD_FALLING] = "invalid_price_trend_falling"
# Validate strongly rising trend threshold
if CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING in user_input and not validate_price_trend_strongly_rising(
user_input[CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING]
):
errors[CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING] = "invalid_price_trend_strongly_rising"
# Validate strongly falling trend threshold
if CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING in user_input and not validate_price_trend_strongly_falling(
user_input[CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING]
):
errors[CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING] = "invalid_price_trend_strongly_falling"
# Cross-validation: Ensure rising < strongly_rising and falling > strongly_falling
if not errors:
rising = user_input.get(CONF_PRICE_TREND_THRESHOLD_RISING)
strongly_rising = user_input.get(CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING)
falling = user_input.get(CONF_PRICE_TREND_THRESHOLD_FALLING)
strongly_falling = user_input.get(CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING)
if rising is not None and strongly_rising is not None and rising >= strongly_rising:
errors[CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING] = (
"invalid_trend_strongly_rising_less_than_rising"
)
if falling is not None and strongly_falling is not None and falling <= strongly_falling:
errors[CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING] = (
"invalid_trend_strongly_falling_greater_than_falling"
)
if not errors: if not errors:
# Store flat data directly in options (no section wrapping) # Store flat data directly in options (no section wrapping)
self._options.update(user_input) self._options.update(user_input)

View file

@ -35,6 +35,8 @@ from custom_components.tibber_prices.const import (
CONF_PRICE_RATING_THRESHOLD_LOW, CONF_PRICE_RATING_THRESHOLD_LOW,
CONF_PRICE_TREND_THRESHOLD_FALLING, CONF_PRICE_TREND_THRESHOLD_FALLING,
CONF_PRICE_TREND_THRESHOLD_RISING, CONF_PRICE_TREND_THRESHOLD_RISING,
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
CONF_RELAXATION_ATTEMPTS_BEST, CONF_RELAXATION_ATTEMPTS_BEST,
CONF_RELAXATION_ATTEMPTS_PEAK, CONF_RELAXATION_ATTEMPTS_PEAK,
CONF_VIRTUAL_TIME_OFFSET_DAYS, CONF_VIRTUAL_TIME_OFFSET_DAYS,
@ -66,6 +68,8 @@ from custom_components.tibber_prices.const import (
DEFAULT_PRICE_RATING_THRESHOLD_LOW, DEFAULT_PRICE_RATING_THRESHOLD_LOW,
DEFAULT_PRICE_TREND_THRESHOLD_FALLING, DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
DEFAULT_PRICE_TREND_THRESHOLD_RISING, DEFAULT_PRICE_TREND_THRESHOLD_RISING,
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
DEFAULT_RELAXATION_ATTEMPTS_BEST, DEFAULT_RELAXATION_ATTEMPTS_BEST,
DEFAULT_RELAXATION_ATTEMPTS_PEAK, DEFAULT_RELAXATION_ATTEMPTS_PEAK,
DEFAULT_VIRTUAL_TIME_OFFSET_DAYS, DEFAULT_VIRTUAL_TIME_OFFSET_DAYS,
@ -86,6 +90,8 @@ from custom_components.tibber_prices.const import (
MAX_PRICE_RATING_THRESHOLD_LOW, MAX_PRICE_RATING_THRESHOLD_LOW,
MAX_PRICE_TREND_FALLING, MAX_PRICE_TREND_FALLING,
MAX_PRICE_TREND_RISING, MAX_PRICE_TREND_RISING,
MAX_PRICE_TREND_STRONGLY_FALLING,
MAX_PRICE_TREND_STRONGLY_RISING,
MAX_RELAXATION_ATTEMPTS, MAX_RELAXATION_ATTEMPTS,
MAX_VOLATILITY_THRESHOLD_HIGH, MAX_VOLATILITY_THRESHOLD_HIGH,
MAX_VOLATILITY_THRESHOLD_MODERATE, MAX_VOLATILITY_THRESHOLD_MODERATE,
@ -99,6 +105,8 @@ from custom_components.tibber_prices.const import (
MIN_PRICE_RATING_THRESHOLD_LOW, MIN_PRICE_RATING_THRESHOLD_LOW,
MIN_PRICE_TREND_FALLING, MIN_PRICE_TREND_FALLING,
MIN_PRICE_TREND_RISING, MIN_PRICE_TREND_RISING,
MIN_PRICE_TREND_STRONGLY_FALLING,
MIN_PRICE_TREND_STRONGLY_RISING,
MIN_RELAXATION_ATTEMPTS, MIN_RELAXATION_ATTEMPTS,
MIN_VOLATILITY_THRESHOLD_HIGH, MIN_VOLATILITY_THRESHOLD_HIGH,
MIN_VOLATILITY_THRESHOLD_MODERATE, MIN_VOLATILITY_THRESHOLD_MODERATE,
@ -745,6 +753,23 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
vol.Optional(
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
default=int(
options.get(
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_PRICE_TREND_STRONGLY_RISING,
max=MAX_PRICE_TREND_STRONGLY_RISING,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional( vol.Optional(
CONF_PRICE_TREND_THRESHOLD_FALLING, CONF_PRICE_TREND_THRESHOLD_FALLING,
default=int( default=int(
@ -762,6 +787,23 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
vol.Optional(
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
default=int(
options.get(
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_PRICE_TREND_STRONGLY_FALLING,
max=MAX_PRICE_TREND_STRONGLY_FALLING,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
),
),
} }
) )

View file

@ -20,6 +20,8 @@ from custom_components.tibber_prices.const import (
MAX_PRICE_RATING_THRESHOLD_LOW, MAX_PRICE_RATING_THRESHOLD_LOW,
MAX_PRICE_TREND_FALLING, MAX_PRICE_TREND_FALLING,
MAX_PRICE_TREND_RISING, MAX_PRICE_TREND_RISING,
MAX_PRICE_TREND_STRONGLY_FALLING,
MAX_PRICE_TREND_STRONGLY_RISING,
MAX_RELAXATION_ATTEMPTS, MAX_RELAXATION_ATTEMPTS,
MAX_VOLATILITY_THRESHOLD_HIGH, MAX_VOLATILITY_THRESHOLD_HIGH,
MAX_VOLATILITY_THRESHOLD_MODERATE, MAX_VOLATILITY_THRESHOLD_MODERATE,
@ -30,6 +32,8 @@ from custom_components.tibber_prices.const import (
MIN_PRICE_RATING_THRESHOLD_LOW, MIN_PRICE_RATING_THRESHOLD_LOW,
MIN_PRICE_TREND_FALLING, MIN_PRICE_TREND_FALLING,
MIN_PRICE_TREND_RISING, MIN_PRICE_TREND_RISING,
MIN_PRICE_TREND_STRONGLY_FALLING,
MIN_PRICE_TREND_STRONGLY_RISING,
MIN_RELAXATION_ATTEMPTS, MIN_RELAXATION_ATTEMPTS,
MIN_VOLATILITY_THRESHOLD_HIGH, MIN_VOLATILITY_THRESHOLD_HIGH,
MIN_VOLATILITY_THRESHOLD_MODERATE, MIN_VOLATILITY_THRESHOLD_MODERATE,
@ -337,3 +341,31 @@ def validate_price_trend_falling(threshold: int) -> bool:
""" """
return MIN_PRICE_TREND_FALLING <= threshold <= MAX_PRICE_TREND_FALLING return MIN_PRICE_TREND_FALLING <= threshold <= MAX_PRICE_TREND_FALLING
def validate_price_trend_strongly_rising(threshold: int) -> bool:
"""
Validate strongly rising price trend threshold.
Args:
threshold: Strongly rising trend threshold percentage (2 to 100)
Returns:
True if threshold is valid (MIN_PRICE_TREND_STRONGLY_RISING to MAX_PRICE_TREND_STRONGLY_RISING)
"""
return MIN_PRICE_TREND_STRONGLY_RISING <= threshold <= MAX_PRICE_TREND_STRONGLY_RISING
def validate_price_trend_strongly_falling(threshold: int) -> bool:
"""
Validate strongly falling price trend threshold.
Args:
threshold: Strongly falling trend threshold percentage (-100 to -2)
Returns:
True if threshold is valid (MIN_PRICE_TREND_STRONGLY_FALLING to MAX_PRICE_TREND_STRONGLY_FALLING)
"""
return MIN_PRICE_TREND_STRONGLY_FALLING <= threshold <= MAX_PRICE_TREND_STRONGLY_FALLING

View file

@ -50,6 +50,8 @@ CONF_PRICE_LEVEL_GAP_TOLERANCE = "price_level_gap_tolerance"
CONF_AVERAGE_SENSOR_DISPLAY = "average_sensor_display" # "median" or "mean" CONF_AVERAGE_SENSOR_DISPLAY = "average_sensor_display" # "median" or "mean"
CONF_PRICE_TREND_THRESHOLD_RISING = "price_trend_threshold_rising" CONF_PRICE_TREND_THRESHOLD_RISING = "price_trend_threshold_rising"
CONF_PRICE_TREND_THRESHOLD_FALLING = "price_trend_threshold_falling" CONF_PRICE_TREND_THRESHOLD_FALLING = "price_trend_threshold_falling"
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING = "price_trend_threshold_strongly_rising"
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING = "price_trend_threshold_strongly_falling"
CONF_VOLATILITY_THRESHOLD_MODERATE = "volatility_threshold_moderate" CONF_VOLATILITY_THRESHOLD_MODERATE = "volatility_threshold_moderate"
CONF_VOLATILITY_THRESHOLD_HIGH = "volatility_threshold_high" CONF_VOLATILITY_THRESHOLD_HIGH = "volatility_threshold_high"
CONF_VOLATILITY_THRESHOLD_VERY_HIGH = "volatility_threshold_very_high" CONF_VOLATILITY_THRESHOLD_VERY_HIGH = "volatility_threshold_very_high"
@ -101,6 +103,10 @@ DEFAULT_PRICE_LEVEL_GAP_TOLERANCE = 1 # Max consecutive intervals to smooth out
DEFAULT_AVERAGE_SENSOR_DISPLAY = "median" # Default: show median in state, mean in attributes DEFAULT_AVERAGE_SENSOR_DISPLAY = "median" # Default: show median in state, mean in attributes
DEFAULT_PRICE_TREND_THRESHOLD_RISING = 3 # Default trend threshold for rising prices (%) DEFAULT_PRICE_TREND_THRESHOLD_RISING = 3 # Default trend threshold for rising prices (%)
DEFAULT_PRICE_TREND_THRESHOLD_FALLING = -3 # Default trend threshold for falling prices (%, negative value) DEFAULT_PRICE_TREND_THRESHOLD_FALLING = -3 # Default trend threshold for falling prices (%, negative value)
# Strong trend thresholds default to 2x the base threshold.
# These are independently configurable to allow fine-tuning of "strongly" detection.
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING = 6 # Default strong rising threshold (%)
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING = -6 # Default strong falling threshold (%, negative value)
# Default volatility thresholds (relative values using coefficient of variation) # Default volatility thresholds (relative values using coefficient of variation)
# Coefficient of variation = (standard_deviation / mean) * 100% # Coefficient of variation = (standard_deviation / mean) * 100%
# These thresholds are unitless and work across different price levels # These thresholds are unitless and work across different price levels
@ -161,6 +167,11 @@ MIN_PRICE_TREND_RISING = 1 # Minimum rising trend threshold
MAX_PRICE_TREND_RISING = 50 # Maximum rising trend threshold MAX_PRICE_TREND_RISING = 50 # Maximum rising trend threshold
MIN_PRICE_TREND_FALLING = -50 # Minimum falling trend threshold (negative) MIN_PRICE_TREND_FALLING = -50 # Minimum falling trend threshold (negative)
MAX_PRICE_TREND_FALLING = -1 # Maximum falling trend threshold (negative) MAX_PRICE_TREND_FALLING = -1 # Maximum falling trend threshold (negative)
# Strong trend thresholds have higher ranges to allow detection of significant moves
MIN_PRICE_TREND_STRONGLY_RISING = 2 # Minimum strongly rising threshold (must be > rising)
MAX_PRICE_TREND_STRONGLY_RISING = 100 # Maximum strongly rising threshold
MIN_PRICE_TREND_STRONGLY_FALLING = -100 # Minimum strongly falling threshold (negative)
MAX_PRICE_TREND_STRONGLY_FALLING = -2 # Maximum strongly falling threshold (must be < falling)
# Gap count and relaxation limits # Gap count and relaxation limits
MIN_GAP_COUNT = 0 # Minimum gap count MIN_GAP_COUNT = 0 # Minimum gap count
@ -447,6 +458,14 @@ VOLATILITY_MODERATE = "MODERATE"
VOLATILITY_HIGH = "HIGH" VOLATILITY_HIGH = "HIGH"
VOLATILITY_VERY_HIGH = "VERY_HIGH" VOLATILITY_VERY_HIGH = "VERY_HIGH"
# Price trend constants (calculated values with 5-level scale)
# Used by trend sensors: momentary, short-term, mid-term, long-term
PRICE_TREND_STRONGLY_FALLING = "strongly_falling"
PRICE_TREND_FALLING = "falling"
PRICE_TREND_STABLE = "stable"
PRICE_TREND_RISING = "rising"
PRICE_TREND_STRONGLY_RISING = "strongly_rising"
# Sensor options (lowercase versions for ENUM device class) # Sensor options (lowercase versions for ENUM device class)
# NOTE: These constants define the valid enum options, but they are not used directly # NOTE: These constants define the valid enum options, but they are not used directly
# in sensor/definitions.py due to import timing issues. Instead, the options are defined inline # in sensor/definitions.py due to import timing issues. Instead, the options are defined inline
@ -472,6 +491,15 @@ VOLATILITY_OPTIONS = [
VOLATILITY_VERY_HIGH.lower(), VOLATILITY_VERY_HIGH.lower(),
] ]
# Trend options for enum sensors (lowercase versions for ENUM device class)
PRICE_TREND_OPTIONS = [
PRICE_TREND_STRONGLY_FALLING,
PRICE_TREND_FALLING,
PRICE_TREND_STABLE,
PRICE_TREND_RISING,
PRICE_TREND_STRONGLY_RISING,
]
# Valid options for best price maximum level filter # Valid options for best price maximum level filter
# Sorted from cheap to expensive: user selects "up to how expensive" # Sorted from cheap to expensive: user selects "up to how expensive"
BEST_PRICE_MAX_LEVEL_OPTIONS = [ BEST_PRICE_MAX_LEVEL_OPTIONS = [
@ -514,6 +542,16 @@ PRICE_RATING_MAPPING = {
PRICE_RATING_HIGH: 1, PRICE_RATING_HIGH: 1,
} }
# Mapping for comparing price trends (used for sorting and automation comparisons)
# Values range from -2 (strongly falling) to +2 (strongly rising), with 0 = stable
PRICE_TREND_MAPPING = {
PRICE_TREND_STRONGLY_FALLING: -2,
PRICE_TREND_FALLING: -1,
PRICE_TREND_STABLE: 0,
PRICE_TREND_RISING: 1,
PRICE_TREND_STRONGLY_RISING: 2,
}
# Icon mapping for price levels (dynamic icons based on level) # Icon mapping for price levels (dynamic icons based on level)
PRICE_LEVEL_ICON_MAPPING = { PRICE_LEVEL_ICON_MAPPING = {
PRICE_LEVEL_VERY_CHEAP: "mdi:gauge-empty", PRICE_LEVEL_VERY_CHEAP: "mdi:gauge-empty",

View file

@ -85,19 +85,25 @@ def get_dynamic_icon(
def get_trend_icon(key: str, value: Any) -> str | None: def get_trend_icon(key: str, value: Any) -> str | None:
"""Get icon for trend sensors.""" """Get icon for trend sensors using 5-level trend scale."""
# Handle next_price_trend_change TIMESTAMP sensor differently # Handle next_price_trend_change TIMESTAMP sensor differently
# (icon based on attributes, not value which is a timestamp) # (icon based on attributes, not value which is a timestamp)
if key == "next_price_trend_change": if key == "next_price_trend_change":
return None # Will be handled by sensor's icon property using attributes return None # Will be handled by sensor's icon property using attributes
if not key.startswith("price_trend_") or not isinstance(value, str): if not key.startswith("price_trend_") and key != "current_price_trend":
return None return None
if not isinstance(value, str):
return None
# 5-level trend icons: strongly uses double arrows, normal uses single
trend_icons = { trend_icons = {
"rising": "mdi:trending-up", "strongly_rising": "mdi:chevron-double-up", # Strong upward movement
"falling": "mdi:trending-down", "rising": "mdi:trending-up", # Normal upward trend
"stable": "mdi:trending-neutral", "stable": "mdi:trending-neutral", # No significant change
"falling": "mdi:trending-down", # Normal downward trend
"strongly_falling": "mdi:chevron-double-down", # Strong downward movement
} }
return trend_icons.get(value) return trend_icons.get(value)

View file

@ -105,6 +105,8 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
# Get configured thresholds from options # Get configured thresholds from options
threshold_rising = self.config.get("price_trend_threshold_rising", 5.0) threshold_rising = self.config.get("price_trend_threshold_rising", 5.0)
threshold_falling = self.config.get("price_trend_threshold_falling", -5.0) threshold_falling = self.config.get("price_trend_threshold_falling", -5.0)
threshold_strongly_rising = self.config.get("price_trend_threshold_strongly_rising", 6.0)
threshold_strongly_falling = self.config.get("price_trend_threshold_strongly_falling", -6.0)
volatility_threshold_moderate = self.config.get("volatility_threshold_moderate", 15.0) volatility_threshold_moderate = self.config.get("volatility_threshold_moderate", 15.0)
volatility_threshold_high = self.config.get("volatility_threshold_high", 30.0) volatility_threshold_high = self.config.get("volatility_threshold_high", 30.0)
@ -115,11 +117,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
lookahead_intervals = self.coordinator.time.minutes_to_intervals(hours * 60) lookahead_intervals = self.coordinator.time.minutes_to_intervals(hours * 60)
# Calculate trend with volatility-adaptive thresholds # Calculate trend with volatility-adaptive thresholds
trend_state, diff_pct = calculate_price_trend( trend_state, diff_pct, trend_value = calculate_price_trend(
current_interval_price, current_interval_price,
future_mean, future_mean,
threshold_rising=threshold_rising, threshold_rising=threshold_rising,
threshold_falling=threshold_falling, threshold_falling=threshold_falling,
threshold_strongly_rising=threshold_strongly_rising,
threshold_strongly_falling=threshold_strongly_falling,
volatility_adjustment=True, # Always enabled volatility_adjustment=True, # Always enabled
lookahead_intervals=lookahead_intervals, lookahead_intervals=lookahead_intervals,
all_intervals=all_intervals, all_intervals=all_intervals,
@ -127,11 +131,14 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
volatility_threshold_high=volatility_threshold_high, volatility_threshold_high=volatility_threshold_high,
) )
# Determine icon color based on trend state # Determine icon color based on trend state (5-level scale)
# Strongly rising/falling uses more intense colors
icon_color = { icon_color = {
"rising": "var(--error-color)", # Red/Orange for rising prices (expensive) "strongly_rising": "var(--error-color)", # Red for strongly rising (very expensive)
"falling": "var(--success-color)", # Green for falling prices (cheaper) "rising": "var(--warning-color)", # Orange/Yellow for rising prices
"stable": "var(--state-icon-color)", # Default gray for stable prices "stable": "var(--state-icon-color)", # Default gray for stable prices
"falling": "var(--success-color)", # Green for falling prices (cheaper)
"strongly_falling": "var(--success-color)", # Green for strongly falling (great deal)
}.get(trend_state, "var(--state-icon-color)") }.get(trend_state, "var(--state-icon-color)")
# Convert prices to display currency unit based on configuration # Convert prices to display currency unit based on configuration
@ -140,6 +147,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
# Store attributes in sensor-specific dictionary AND cache the trend value # Store attributes in sensor-specific dictionary AND cache the trend value
self._trend_attributes = { self._trend_attributes = {
"timestamp": next_interval_start, "timestamp": next_interval_start,
"trend_value": trend_value,
f"trend_{hours}h_%": round(diff_pct, 1), f"trend_{hours}h_%": round(diff_pct, 1),
f"next_{hours}h_avg": round(future_mean * factor, 2), f"next_{hours}h_avg": round(future_mean * factor, 2),
"interval_count": lookahead_intervals, "interval_count": lookahead_intervals,
@ -414,6 +422,8 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
return { return {
"rising": self.config.get("price_trend_threshold_rising", 5.0), "rising": self.config.get("price_trend_threshold_rising", 5.0),
"falling": self.config.get("price_trend_threshold_falling", -5.0), "falling": self.config.get("price_trend_threshold_falling", -5.0),
"strongly_rising": self.config.get("price_trend_threshold_strongly_rising", 6.0),
"strongly_falling": self.config.get("price_trend_threshold_strongly_falling", -6.0),
"moderate": self.config.get("volatility_threshold_moderate", 15.0), "moderate": self.config.get("volatility_threshold_moderate", 15.0),
"high": self.config.get("volatility_threshold_high", 30.0), "high": self.config.get("volatility_threshold_high", 30.0),
} }
@ -428,7 +438,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
current_index: Index of current interval current_index: Index of current interval
Returns: Returns:
Momentum direction: "rising", "falling", or "stable" Momentum direction: "strongly_rising", "rising", "stable", "falling", or "strongly_falling"
""" """
# Look back 1 hour (4 intervals) for quick reaction # Look back 1 hour (4 intervals) for quick reaction
@ -451,15 +461,25 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
weighted_sum = sum(price * weight for price, weight in zip(trailing_prices, weights, strict=True)) weighted_sum = sum(price * weight for price, weight in zip(trailing_prices, weights, strict=True))
weighted_avg = weighted_sum / sum(weights) weighted_avg = weighted_sum / sum(weights)
# Calculate momentum with 3% threshold # Calculate momentum with thresholds
# Using same logic as 5-level trend: 3% for normal, 6% (2x) for strong
momentum_threshold = 0.03 momentum_threshold = 0.03
diff = (current_price - weighted_avg) / weighted_avg strong_momentum_threshold = 0.06
diff = (current_price - weighted_avg) / abs(weighted_avg) if weighted_avg != 0 else 0
if diff > momentum_threshold: # Determine momentum level based on thresholds
return "rising" if diff >= strong_momentum_threshold:
if diff < -momentum_threshold: momentum = "strongly_rising"
return "falling" elif diff > momentum_threshold:
return "stable" momentum = "rising"
elif diff <= -strong_momentum_threshold:
momentum = "strongly_falling"
elif diff < -momentum_threshold:
momentum = "falling"
else:
momentum = "stable"
return momentum
def _combine_momentum_with_future( def _combine_momentum_with_future(
self, self,
@ -472,43 +492,60 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
""" """
Combine momentum analysis with future outlook to determine final trend. Combine momentum analysis with future outlook to determine final trend.
Uses 5-level scale: strongly_rising, rising, stable, falling, strongly_falling.
Momentum intensity is preserved when future confirms the trend direction.
Args: Args:
current_momentum: Current momentum direction (rising/falling/stable) current_momentum: Current momentum direction (5-level scale)
current_price: Current interval price current_price: Current interval price
future_mean: Average price in future window future_mean: Average price in future window
context: Dict with all_intervals, current_index, lookahead_intervals, thresholds context: Dict with all_intervals, current_index, lookahead_intervals, thresholds
Returns: Returns:
Final trend direction: "rising", "falling", or "stable" Final trend direction (5-level scale)
""" """
if current_momentum == "rising": # Use calculate_price_trend for consistency with 5-level logic
# We're in uptrend - does it continue?
return "rising" if future_mean >= current_price * 0.98 else "falling"
if current_momentum == "falling":
# We're in downtrend - does it continue?
return "falling" if future_mean <= current_price * 1.02 else "rising"
# current_momentum == "stable" - what's coming?
all_intervals = context["all_intervals"] all_intervals = context["all_intervals"]
current_index = context["current_index"] current_index = context["current_index"]
lookahead_intervals = context["lookahead_intervals"] lookahead_intervals = context["lookahead_intervals"]
thresholds = context["thresholds"] thresholds = context["thresholds"]
lookahead_for_volatility = all_intervals[current_index : current_index + lookahead_intervals] lookahead_for_volatility = all_intervals[current_index : current_index + lookahead_intervals]
trend_state, _ = calculate_price_trend( future_trend, _, _ = calculate_price_trend(
current_price, current_price,
future_mean, future_mean,
threshold_rising=thresholds["rising"], threshold_rising=thresholds["rising"],
threshold_falling=thresholds["falling"], threshold_falling=thresholds["falling"],
threshold_strongly_rising=thresholds["strongly_rising"],
threshold_strongly_falling=thresholds["strongly_falling"],
volatility_adjustment=True, volatility_adjustment=True,
lookahead_intervals=lookahead_intervals, lookahead_intervals=lookahead_intervals,
all_intervals=lookahead_for_volatility, all_intervals=lookahead_for_volatility,
volatility_threshold_moderate=thresholds["moderate"], volatility_threshold_moderate=thresholds["moderate"],
volatility_threshold_high=thresholds["high"], volatility_threshold_high=thresholds["high"],
) )
return trend_state
# Check if momentum and future trend are aligned (same direction)
momentum_rising = current_momentum in ("rising", "strongly_rising")
momentum_falling = current_momentum in ("falling", "strongly_falling")
future_rising = future_trend in ("rising", "strongly_rising")
future_falling = future_trend in ("falling", "strongly_falling")
if momentum_rising and future_rising:
# Both indicate rising - use the stronger signal
if current_momentum == "strongly_rising" or future_trend == "strongly_rising":
return "strongly_rising"
return "rising"
if momentum_falling and future_falling:
# Both indicate falling - use the stronger signal
if current_momentum == "strongly_falling" or future_trend == "strongly_falling":
return "strongly_falling"
return "falling"
# Conflicting signals or stable momentum - trust future trend calculation
return future_trend
def _calculate_standard_trend( def _calculate_standard_trend(
self, self,
@ -534,11 +571,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
current_price = float(current_interval["total"]) current_price = float(current_interval["total"])
standard_lookahead_volatility = all_intervals[current_index : current_index + standard_lookahead] standard_lookahead_volatility = all_intervals[current_index : current_index + standard_lookahead]
current_trend_3h, _ = calculate_price_trend( current_trend_3h, _, _ = calculate_price_trend(
current_price, current_price,
standard_future_mean, standard_future_mean,
threshold_rising=thresholds["rising"], threshold_rising=thresholds["rising"],
threshold_falling=thresholds["falling"], threshold_falling=thresholds["falling"],
threshold_strongly_rising=thresholds["strongly_rising"],
threshold_strongly_falling=thresholds["strongly_falling"],
volatility_adjustment=True, volatility_adjustment=True,
lookahead_intervals=standard_lookahead, lookahead_intervals=standard_lookahead,
all_intervals=standard_lookahead_volatility, all_intervals=standard_lookahead_volatility,
@ -606,11 +645,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
# Calculate trend at this past point # Calculate trend at this past point
lookahead_for_volatility = all_intervals[i : i + intervals_in_3h] lookahead_for_volatility = all_intervals[i : i + intervals_in_3h]
trend_state, _ = calculate_price_trend( trend_state, _, _ = calculate_price_trend(
price, price,
future_mean, future_mean,
threshold_rising=thresholds["rising"], threshold_rising=thresholds["rising"],
threshold_falling=thresholds["falling"], threshold_falling=thresholds["falling"],
threshold_strongly_rising=thresholds["strongly_rising"],
threshold_strongly_falling=thresholds["strongly_falling"],
volatility_adjustment=True, volatility_adjustment=True,
lookahead_intervals=intervals_in_3h, lookahead_intervals=intervals_in_3h,
all_intervals=lookahead_for_volatility, all_intervals=lookahead_for_volatility,
@ -678,11 +719,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
# Calculate trend at this future point # Calculate trend at this future point
lookahead_for_volatility = all_intervals[i : i + intervals_in_3h] lookahead_for_volatility = all_intervals[i : i + intervals_in_3h]
trend_state, _ = calculate_price_trend( trend_state, _, _ = calculate_price_trend(
current_price, current_price,
future_mean, future_mean,
threshold_rising=thresholds["rising"], threshold_rising=thresholds["rising"],
threshold_falling=thresholds["falling"], threshold_falling=thresholds["falling"],
threshold_strongly_rising=thresholds["strongly_rising"],
threshold_strongly_falling=thresholds["strongly_falling"],
volatility_adjustment=True, volatility_adjustment=True,
lookahead_intervals=intervals_in_3h, lookahead_intervals=intervals_in_3h,
all_intervals=lookahead_for_volatility, all_intervals=lookahead_for_volatility,

View file

@ -987,11 +987,13 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
key = self.entity_description.key key = self.entity_description.key
value = self.native_value value = self.native_value
# Icon mapping for trend directions # Icon mapping for trend directions (5-level scale)
trend_icons = { trend_icons = {
"strongly_rising": "mdi:chevron-double-up",
"rising": "mdi:trending-up", "rising": "mdi:trending-up",
"falling": "mdi:trending-down",
"stable": "mdi:trending-neutral", "stable": "mdi:trending-neutral",
"falling": "mdi:trending-down",
"strongly_falling": "mdi:chevron-double-down",
} }
# Special handling for next_price_trend_change: Icon based on direction attribute # Special handling for next_price_trend_change: Icon based on direction attribute

View file

@ -548,7 +548,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: trending-up/trending-down/trending-neutral based on current trend icon="mdi:trending-up", # Dynamic: trending-up/trending-down/trending-neutral based on current trend
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
), ),
# Next trend change sensor (when will trend change?) # Next trend change sensor (when will trend change?)
@ -570,7 +570,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
), ),
SensorEntityDescription( SensorEntityDescription(
@ -580,7 +580,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
), ),
SensorEntityDescription( SensorEntityDescription(
@ -590,7 +590,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
), ),
SensorEntityDescription( SensorEntityDescription(
@ -600,7 +600,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
), ),
SensorEntityDescription( SensorEntityDescription(
@ -610,7 +610,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
), ),
# Disabled by default: 6h, 8h, 12h # Disabled by default: 6h, 8h, 12h
@ -621,7 +621,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
SensorEntityDescription( SensorEntityDescription(
@ -631,7 +631,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
SensorEntityDescription( SensorEntityDescription(
@ -641,7 +641,7 @@ FUTURE_TREND_SENSORS = (
icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
state_class=None, # Enum values: no statistics state_class=None, # Enum values: no statistics
options=["rising", "falling", "stable"], options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
) )

View file

@ -283,14 +283,18 @@
}, },
"price_trend": { "price_trend": {
"title": "📈 Preistrend-Schwellenwerte", "title": "📈 Preistrend-Schwellenwerte",
"description": "**Konfiguriere 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.**", "description": "**Konfiguriere 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.\n\n**5-Stufen-Skala:** Nutzt stark_fallend (-2), fallend (-1), stabil (0), steigend (+1), stark_steigend (+2) für Automations-Vergleiche über das trend_value Attribut.",
"data": { "data": {
"price_trend_threshold_rising": "Steigend-Schwelle", "price_trend_threshold_rising": "Steigend-Schwelle",
"price_trend_threshold_falling": "Fallend-Schwelle" "price_trend_threshold_strongly_rising": "Stark steigend-Schwelle",
"price_trend_threshold_falling": "Fallend-Schwelle",
"price_trend_threshold_strongly_falling": "Stark fallend-Schwelle"
}, },
"data_description": { "data_description": {
"price_trend_threshold_rising": "Prozentwert, um wie viel der Durchschnitt der nächsten N Stunden über dem aktuellen Preis liegen muss, damit der Trend als 'steigend' gilt. Beispiel: 5 bedeutet Durchschnitt ist mindestens 5% höher → Preise werden steigen. Typische Werte: 5-15%. Standard: 5%", "price_trend_threshold_rising": "Prozentwert, um wie viel der Durchschnitt der nächsten N Stunden über dem aktuellen Preis liegen muss, damit der Trend als 'steigend' gilt. Beispiel: 3 bedeutet Durchschnitt ist mindestens 3% höher → Preise werden steigen. Typische Werte: 3-10%. Standard: 3%",
"price_trend_threshold_falling": "Prozentwert (negativ), um wie viel der Durchschnitt der nächsten N Stunden unter dem aktuellen Preis liegen muss, damit der Trend als 'fallend' gilt. Beispiel: -5 bedeutet Durchschnitt ist mindestens 5% niedriger → Preise werden fallen. Typische Werte: -5 bis -15%. Standard: -5%" "price_trend_threshold_strongly_rising": "Prozentwert für 'stark steigend'-Trend. Muss höher sein als die steigend-Schwelle. Beispiel: 6 bedeutet Durchschnitt ist mindestens 6% höher → Preise werden deutlich steigen. Typische Werte: 6-15%. Standard: 6%",
"price_trend_threshold_falling": "Prozentwert (negativ), um wie viel der Durchschnitt der nächsten N Stunden unter dem aktuellen Preis liegen muss, damit der Trend als 'fallend' gilt. Beispiel: -3 bedeutet Durchschnitt ist mindestens 3% niedriger → Preise werden fallen. Typische Werte: -3 bis -10%. Standard: -3%",
"price_trend_threshold_strongly_falling": "Prozentwert (negativ) für 'stark fallend'-Trend. Muss niedriger (negativer) sein als die fallend-Schwelle. Beispiel: -6 bedeutet Durchschnitt ist mindestens 6% niedriger → Preise werden deutlich fallen. Typische Werte: -6 bis -15%. Standard: -6%"
}, },
"submit": "↩ Speichern & Zurück" "submit": "↩ Speichern & Zurück"
}, },
@ -356,7 +360,11 @@
"invalid_volatility_threshold_very_high": "Sehr hohe Volatilitätsschwelle muss zwischen 35% und 80% liegen", "invalid_volatility_threshold_very_high": "Sehr hohe Volatilitätsschwelle muss zwischen 35% und 80% liegen",
"invalid_volatility_thresholds": "Schwellenwerte müssen aufsteigend sein: moderat < hoch < sehr hoch", "invalid_volatility_thresholds": "Schwellenwerte müssen aufsteigend sein: moderat < hoch < sehr hoch",
"invalid_price_trend_rising": "Steigender Trendschwellenwert muss zwischen 1% und 50% liegen", "invalid_price_trend_rising": "Steigender Trendschwellenwert muss zwischen 1% und 50% liegen",
"invalid_price_trend_falling": "Fallender Trendschwellenwert muss zwischen -50% und -1% liegen" "invalid_price_trend_falling": "Fallender Trendschwellenwert muss zwischen -50% und -1% liegen",
"invalid_price_trend_strongly_rising": "Stark steigender Trendschwellenwert muss zwischen 2% und 100% liegen",
"invalid_price_trend_strongly_falling": "Stark fallender Trendschwellenwert muss zwischen -100% und -2% liegen",
"invalid_trend_strongly_rising_less_than_rising": "Stark steigend-Schwelle muss größer als steigend-Schwelle sein",
"invalid_trend_strongly_falling_greater_than_falling": "Stark fallend-Schwelle muss kleiner (negativer) als fallend-Schwelle sein"
}, },
"abort": { "abort": {
"entry_not_found": "Tibber Konfigurationseintrag nicht gefunden.", "entry_not_found": "Tibber Konfigurationseintrag nicht gefunden.",
@ -592,73 +600,91 @@
"price_trend_1h": { "price_trend_1h": {
"name": "Preistrend (1h)", "name": "Preistrend (1h)",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"price_trend_2h": { "price_trend_2h": {
"name": "Preistrend (2h)", "name": "Preistrend (2h)",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"price_trend_3h": { "price_trend_3h": {
"name": "Preistrend (3h)", "name": "Preistrend (3h)",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"price_trend_4h": { "price_trend_4h": {
"name": "Preistrend (4h)", "name": "Preistrend (4h)",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"price_trend_5h": { "price_trend_5h": {
"name": "Preistrend (5h)", "name": "Preistrend (5h)",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"price_trend_6h": { "price_trend_6h": {
"name": "Preistrend (6h)", "name": "Preistrend (6h)",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"price_trend_8h": { "price_trend_8h": {
"name": "Preistrend (8h)", "name": "Preistrend (8h)",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"price_trend_12h": { "price_trend_12h": {
"name": "Preistrend (12h)", "name": "Preistrend (12h)",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"current_price_trend": { "current_price_trend": {
"name": "Aktueller Preistrend", "name": "Aktueller Preistrend",
"state": { "state": {
"strongly_rising": "Stark steigend",
"rising": "Steigend", "rising": "Steigend",
"stable": "Stabil",
"falling": "Fallend", "falling": "Fallend",
"stable": "Stabil" "strongly_falling": "Stark fallend"
} }
}, },
"next_price_trend_change": { "next_price_trend_change": {

View file

@ -294,14 +294,18 @@
}, },
"price_trend": { "price_trend": {
"title": "📈 Price Trend Thresholds", "title": "📈 Price Trend Thresholds",
"description": "**Configure thresholds for price trend sensors. These sensors compare current price with the average of the next N hours to determine if prices are rising, falling, or stable.**", "description": "**Configure thresholds for price trend sensors.** These sensors compare current price with the average of the next N hours to determine if prices are rising, falling, or stable.\n\n**5-Level Scale:** Uses strongly_falling (-2), falling (-1), stable (0), rising (+1), strongly_rising (+2) for automation comparisons via trend_value attribute.",
"data": { "data": {
"price_trend_threshold_rising": "Rising Threshold", "price_trend_threshold_rising": "Rising Threshold",
"price_trend_threshold_falling": "Falling Threshold" "price_trend_threshold_strongly_rising": "Strongly Rising Threshold",
"price_trend_threshold_falling": "Falling Threshold",
"price_trend_threshold_strongly_falling": "Strongly Falling Threshold"
}, },
"data_description": { "data_description": {
"price_trend_threshold_rising": "Percentage that the average of the next N hours must be above the current price to qualify as 'rising' trend. Example: 5 means average is at least 5% higher → prices will rise. Typical values: 5-15%. Default: 5%", "price_trend_threshold_rising": "Percentage that the average of the next N hours must be above the current price to qualify as 'rising' trend. Example: 3 means average is at least 3% higher → prices will rise. Typical values: 3-10%. Default: 3%",
"price_trend_threshold_falling": "Percentage (negative) that the average of the next N hours must be below the current price to qualify as 'falling' trend. Example: -5 means average is at least 5% lower → prices will fall. Typical values: -5 to -15%. Default: -5%" "price_trend_threshold_strongly_rising": "Percentage for 'strongly rising' trend. Must be higher than rising threshold. Example: 6 means average is at least 6% higher → prices will rise significantly. Typical values: 6-15%. Default: 6%",
"price_trend_threshold_falling": "Percentage (negative) that the average of the next N hours must be below the current price to qualify as 'falling' trend. Example: -3 means average is at least 3% lower → prices will fall. Typical values: -3 to -10%. Default: -3%",
"price_trend_threshold_strongly_falling": "Percentage (negative) for 'strongly falling' trend. Must be lower (more negative) than falling threshold. Example: -6 means average is at least 6% lower → prices will fall significantly. Typical values: -6 to -15%. Default: -6%"
}, },
"submit": "↩ Save & Back" "submit": "↩ Save & Back"
}, },
@ -356,7 +360,11 @@
"invalid_volatility_threshold_very_high": "Very high volatility threshold must be between 35% and 80%", "invalid_volatility_threshold_very_high": "Very high volatility threshold must be between 35% and 80%",
"invalid_volatility_thresholds": "Thresholds must be in ascending order: moderate < high < very high", "invalid_volatility_thresholds": "Thresholds must be in ascending order: moderate < high < very high",
"invalid_price_trend_rising": "Rising trend threshold must be between 1% and 50%", "invalid_price_trend_rising": "Rising trend threshold must be between 1% and 50%",
"invalid_price_trend_falling": "Falling trend threshold must be between -50% and -1%" "invalid_price_trend_falling": "Falling trend threshold must be between -50% and -1%",
"invalid_price_trend_strongly_rising": "Strongly rising trend threshold must be between 2% and 100%",
"invalid_price_trend_strongly_falling": "Strongly falling trend threshold must be between -100% and -2%",
"invalid_trend_strongly_rising_less_than_rising": "Strongly rising threshold must be greater than rising threshold",
"invalid_trend_strongly_falling_greater_than_falling": "Strongly falling threshold must be less (more negative) than falling threshold"
}, },
"abort": { "abort": {
"entry_not_found": "Tibber configuration entry not found.", "entry_not_found": "Tibber configuration entry not found.",
@ -592,73 +600,91 @@
"price_trend_1h": { "price_trend_1h": {
"name": "Price Trend (1h)", "name": "Price Trend (1h)",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"price_trend_2h": { "price_trend_2h": {
"name": "Price Trend (2h)", "name": "Price Trend (2h)",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"price_trend_3h": { "price_trend_3h": {
"name": "Price Trend (3h)", "name": "Price Trend (3h)",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"price_trend_4h": { "price_trend_4h": {
"name": "Price Trend (4h)", "name": "Price Trend (4h)",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"price_trend_5h": { "price_trend_5h": {
"name": "Price Trend (5h)", "name": "Price Trend (5h)",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"price_trend_6h": { "price_trend_6h": {
"name": "Price Trend (6h)", "name": "Price Trend (6h)",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"price_trend_8h": { "price_trend_8h": {
"name": "Price Trend (8h)", "name": "Price Trend (8h)",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"price_trend_12h": { "price_trend_12h": {
"name": "Price Trend (12h)", "name": "Price Trend (12h)",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"current_price_trend": { "current_price_trend": {
"name": "Current Price Trend", "name": "Current Price Trend",
"state": { "state": {
"strongly_rising": "Strongly Rising",
"rising": "Rising", "rising": "Rising",
"stable": "Stable",
"falling": "Falling", "falling": "Falling",
"stable": "Stable" "strongly_falling": "Strongly Falling"
} }
}, },
"next_price_trend_change": { "next_price_trend_change": {

View file

@ -283,14 +283,18 @@
}, },
"price_trend": { "price_trend": {
"title": "📈 Pristrendterskler", "title": "📈 Pristrendterskler",
"description": "**Konfigurer terskler for pristrendsensorer. Disse sensorene sammenligner nåværende pris med gjennomsnittet av de neste N timene for å bestemme om prisene stiger, faller eller er stabile.**", "description": "**Konfigurer terskler for pristrendsensorer. Disse sensorene sammenligner nåværende pris med gjennomsnittet av de neste N timene for å bestemme om prisene stiger sterkt, stiger, er stabile, faller eller faller sterkt.**",
"data": { "data": {
"price_trend_threshold_rising": "Stigende terskel", "price_trend_threshold_rising": "Stigende terskel",
"price_trend_threshold_falling": "Fallende terskel" "price_trend_threshold_strongly_rising": "Sterkt stigende terskel",
"price_trend_threshold_falling": "Fallende terskel",
"price_trend_threshold_strongly_falling": "Sterkt fallende terskel"
}, },
"data_description": { "data_description": {
"price_trend_threshold_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'stigende' trend. Eksempel: 5 betyr gjennomsnittet er minst 5% høyere → prisene vil stige. Typiske verdier: 5-15%. Standard: 5%", "price_trend_threshold_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'stigende' trend. Eksempel: 3 betyr gjennomsnittet er minst 3% høyere → prisene vil stige. Typiske verdier: 3-10%. Standard: 3%",
"price_trend_threshold_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'synkende' trend. Eksempel: -5 betyr gjennomsnittet er minst 5% lavere → prisene vil falle. Typiske verdier: -5 til -15%. Standard: -5%" "price_trend_threshold_strongly_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'sterkt stigende' trend. Må være høyere enn stigende terskel. Typiske verdier: 6-20%. Standard: 6%",
"price_trend_threshold_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'synkende' trend. Eksempel: -3 betyr gjennomsnittet er minst 3% lavere → prisene vil falle. Typiske verdier: -3 til -10%. Standard: -3%",
"price_trend_threshold_strongly_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'sterkt synkende' trend. Må være lavere (mer negativ) enn fallende terskel. Typiske verdier: -6 til -20%. Standard: -6%"
}, },
"submit": "↩ Lagre & tilbake" "submit": "↩ Lagre & tilbake"
}, },
@ -356,7 +360,11 @@
"invalid_volatility_threshold_very_high": "Svært høy volatilitetsgrense må være mellom 35% og 80%", "invalid_volatility_threshold_very_high": "Svært høy volatilitetsgrense må være mellom 35% og 80%",
"invalid_volatility_thresholds": "Grensene må være i stigende rekkefølge: moderat < høy < svært høy", "invalid_volatility_thresholds": "Grensene må være i stigende rekkefølge: moderat < høy < svært høy",
"invalid_price_trend_rising": "Stigende trendgrense må være mellom 1% og 50%", "invalid_price_trend_rising": "Stigende trendgrense må være mellom 1% og 50%",
"invalid_price_trend_falling": "Fallende trendgrense må være mellom -50% og -1%" "invalid_price_trend_falling": "Fallende trendgrense må være mellom -50% og -1%",
"invalid_price_trend_strongly_rising": "Sterkt stigende trendgrense må være mellom 2% og 100%",
"invalid_price_trend_strongly_falling": "Sterkt fallende trendgrense må være mellom -100% og -2%",
"invalid_trend_strongly_rising_less_than_rising": "Sterkt stigende-grense må være høyere enn stigende-grense",
"invalid_trend_strongly_falling_greater_than_falling": "Sterkt fallende-grense må være lavere (mer negativ) enn fallende-grense"
}, },
"abort": { "abort": {
"entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet.", "entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet.",
@ -592,73 +600,91 @@
"price_trend_1h": { "price_trend_1h": {
"name": "Pristrend (1t)", "name": "Pristrend (1t)",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"price_trend_2h": { "price_trend_2h": {
"name": "Pristrend (2t)", "name": "Pristrend (2t)",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"price_trend_3h": { "price_trend_3h": {
"name": "Pristrend (3t)", "name": "Pristrend (3t)",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"price_trend_4h": { "price_trend_4h": {
"name": "Pristrend (4t)", "name": "Pristrend (4t)",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"price_trend_5h": { "price_trend_5h": {
"name": "Pristrend (5t)", "name": "Pristrend (5t)",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"price_trend_6h": { "price_trend_6h": {
"name": "Pristrend (6t)", "name": "Pristrend (6t)",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"price_trend_8h": { "price_trend_8h": {
"name": "Pristrend (8t)", "name": "Pristrend (8t)",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"price_trend_12h": { "price_trend_12h": {
"name": "Pristrend (12t)", "name": "Pristrend (12t)",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"current_price_trend": { "current_price_trend": {
"name": "Nåværende pristrend", "name": "Nåværende pristrend",
"state": { "state": {
"strongly_rising": "Sterkt stigende",
"rising": "Stigende", "rising": "Stigende",
"stable": "Stabil",
"falling": "Fallende", "falling": "Fallende",
"stable": "Stabil" "strongly_falling": "Sterkt fallende"
} }
}, },
"next_price_trend_change": { "next_price_trend_change": {

View file

@ -283,14 +283,18 @@
}, },
"price_trend": { "price_trend": {
"title": "📈 Prijstrend Drempelwaarden", "title": "📈 Prijstrend Drempelwaarden",
"description": "**Configureer drempelwaarden voor prijstrend sensoren. Deze sensoren vergelijken de huidige prijs met het gemiddelde van de volgende N uur om te bepalen of prijzen stijgen, dalen of stabiel zijn.**", "description": "**Configureer drempelwaarden voor prijstrend sensoren. Deze sensoren vergelijken de huidige prijs met het gemiddelde van de volgende N uur om te bepalen of prijzen sterk stijgen, stijgen, stabiel zijn, dalen of sterk dalen.**",
"data": { "data": {
"price_trend_threshold_rising": "Stijgende Drempel", "price_trend_threshold_rising": "Stijgende Drempel",
"price_trend_threshold_falling": "Dalende Drempel" "price_trend_threshold_strongly_rising": "Sterk Stijgende Drempel",
"price_trend_threshold_falling": "Dalende Drempel",
"price_trend_threshold_strongly_falling": "Sterk Dalende Drempel"
}, },
"data_description": { "data_description": {
"price_trend_threshold_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'stijgende' trend. Voorbeeld: 5 betekent dat het gemiddelde minimaal 5% hoger is → prijzen zullen stijgen. Typische waarden: 5-15%. Standaard: 5%", "price_trend_threshold_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'stijgende' trend. Voorbeeld: 3 betekent dat het gemiddelde minimaal 3% hoger is → prijzen zullen stijgen. Typische waarden: 3-10%. Standaard: 3%",
"price_trend_threshold_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'dalende' trend. Voorbeeld: -5 betekent dat het gemiddelde minimaal 5% lager is → prijzen zullen dalen. Typische waarden: -5 tot -15%. Standaard: -5%" "price_trend_threshold_strongly_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'sterk stijgende' trend. Moet hoger zijn dan stijgende drempel. Typische waarden: 6-20%. Standaard: 6%",
"price_trend_threshold_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'dalende' trend. Voorbeeld: -3 betekent dat het gemiddelde minimaal 3% lager is → prijzen zullen dalen. Typische waarden: -3 tot -10%. Standaard: -3%",
"price_trend_threshold_strongly_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'sterk dalende' trend. Moet lager (meer negatief) zijn dan dalende drempel. Typische waarden: -6 tot -20%. Standaard: -6%"
}, },
"submit": "↩ Opslaan & Terug" "submit": "↩ Opslaan & Terug"
}, },
@ -356,7 +360,11 @@
"invalid_volatility_threshold_very_high": "Zeer hoge volatiliteit drempel moet tussen 35% en 80% zijn", "invalid_volatility_threshold_very_high": "Zeer hoge volatiliteit drempel moet tussen 35% en 80% zijn",
"invalid_volatility_thresholds": "Drempelwaarden moeten in oplopende volgorde zijn: gematigd < hoog < zeer hoog", "invalid_volatility_thresholds": "Drempelwaarden moeten in oplopende volgorde zijn: gematigd < hoog < zeer hoog",
"invalid_price_trend_rising": "Stijgende trend drempel moet tussen 1% en 50% zijn", "invalid_price_trend_rising": "Stijgende trend drempel moet tussen 1% en 50% zijn",
"invalid_price_trend_falling": "Dalende trend drempel moet tussen -50% en -1% zijn" "invalid_price_trend_falling": "Dalende trend drempel moet tussen -50% en -1% zijn",
"invalid_price_trend_strongly_rising": "Sterk stijgende trend drempel moet tussen 2% en 100% zijn",
"invalid_price_trend_strongly_falling": "Sterk dalende trend drempel moet tussen -100% en -2% zijn",
"invalid_trend_strongly_rising_less_than_rising": "Sterk stijgende drempel moet hoger zijn dan stijgende drempel",
"invalid_trend_strongly_falling_greater_than_falling": "Sterk dalende drempel moet lager (meer negatief) zijn dan dalende drempel"
}, },
"abort": { "abort": {
"entry_not_found": "Tibber-configuratie-item niet gevonden.", "entry_not_found": "Tibber-configuratie-item niet gevonden.",
@ -592,73 +600,91 @@
"price_trend_1h": { "price_trend_1h": {
"name": "Prijstrend (1u)", "name": "Prijstrend (1u)",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"price_trend_2h": { "price_trend_2h": {
"name": "Prijstrend (2u)", "name": "Prijstrend (2u)",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"price_trend_3h": { "price_trend_3h": {
"name": "Prijstrend (3u)", "name": "Prijstrend (3u)",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"price_trend_4h": { "price_trend_4h": {
"name": "Prijstrend (4u)", "name": "Prijstrend (4u)",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"price_trend_5h": { "price_trend_5h": {
"name": "Prijstrend (5u)", "name": "Prijstrend (5u)",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"price_trend_6h": { "price_trend_6h": {
"name": "Prijstrend (6u)", "name": "Prijstrend (6u)",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"price_trend_8h": { "price_trend_8h": {
"name": "Prijstrend (8u)", "name": "Prijstrend (8u)",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"price_trend_12h": { "price_trend_12h": {
"name": "Prijstrend (12u)", "name": "Prijstrend (12u)",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"current_price_trend": { "current_price_trend": {
"name": "Huidige Prijstrend", "name": "Huidige Prijstrend",
"state": { "state": {
"strongly_rising": "Sterk stijgend",
"rising": "Stijgend", "rising": "Stijgend",
"stable": "Stabiel",
"falling": "Dalend", "falling": "Dalend",
"stable": "Stabiel" "strongly_falling": "Sterk dalend"
} }
}, },
"next_price_trend_change": { "next_price_trend_change": {

View file

@ -283,14 +283,18 @@
}, },
"price_trend": { "price_trend": {
"title": "📈 Pristrendtrösklar", "title": "📈 Pristrendtrösklar",
"description": "**Konfigurera tröskelvärden för pristrendsensorer. Dessa sensorer jämför aktuellt pris med genomsnittet av de nästa N timmarna för att bestämma om priserna stiger, faller eller är stabila.**", "description": "**Konfigurera tröskelvärden för pristrendsensorer. Dessa sensorer jämför aktuellt pris med genomsnittet av de nästa N timmarna för att bestämma om priserna stiger kraftigt, stiger, är stabila, faller eller faller kraftigt.**",
"data": { "data": {
"price_trend_threshold_rising": "Stigande tröskel", "price_trend_threshold_rising": "Stigande tröskel",
"price_trend_threshold_falling": "Fallande tröskel" "price_trend_threshold_strongly_rising": "Kraftigt stigande tröskel",
"price_trend_threshold_falling": "Fallande tröskel",
"price_trend_threshold_strongly_falling": "Kraftigt fallande tröskel"
}, },
"data_description": { "data_description": {
"price_trend_threshold_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'stigande' trend. Exempel: 5 betyder att genomsnittet är minst 5% högre → priserna kommer att stiga. Typiska värden: 5-15%. Standard: 5%", "price_trend_threshold_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'stigande' trend. Exempel: 3 betyder att genomsnittet är minst 3% högre → priserna kommer att stiga. Typiska värden: 3-10%. Standard: 3%",
"price_trend_threshold_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'fallande' trend. Exempel: -5 betyder att genomsnittet är minst 5% lägre → priserna kommer att falla. Typiska värden: -5 till -15%. Standard: -5%" "price_trend_threshold_strongly_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'kraftigt stigande' trend. Måste vara högre än stigande tröskel. Typiska värden: 6-20%. Standard: 6%",
"price_trend_threshold_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'fallande' trend. Exempel: -3 betyder att genomsnittet är minst 3% lägre → priserna kommer att falla. Typiska värden: -3 till -10%. Standard: -3%",
"price_trend_threshold_strongly_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'kraftigt fallande' trend. Måste vara lägre (mer negativ) än fallande tröskel. Typiska värden: -6 till -20%. Standard: -6%"
}, },
"submit": "↩ Spara & tillbaka" "submit": "↩ Spara & tillbaka"
}, },
@ -356,7 +360,11 @@
"invalid_volatility_threshold_very_high": "Mycket hög volatilitetströskel måste vara mellan 35% och 80%", "invalid_volatility_threshold_very_high": "Mycket hög volatilitetströskel måste vara mellan 35% och 80%",
"invalid_volatility_thresholds": "Trösklar måste vara i stigande ordning: måttlig < hög < mycket hög", "invalid_volatility_thresholds": "Trösklar måste vara i stigande ordning: måttlig < hög < mycket hög",
"invalid_price_trend_rising": "Stigande trendtröskel måste vara mellan 1% och 50%", "invalid_price_trend_rising": "Stigande trendtröskel måste vara mellan 1% och 50%",
"invalid_price_trend_falling": "Fallande trendtröskel måste vara mellan -50% och -1%" "invalid_price_trend_falling": "Fallande trendtröskel måste vara mellan -50% och -1%",
"invalid_price_trend_strongly_rising": "Kraftigt stigande trendtröskel måste vara mellan 2% och 100%",
"invalid_price_trend_strongly_falling": "Kraftigt fallande trendtröskel måste vara mellan -100% och -2%",
"invalid_trend_strongly_rising_less_than_rising": "Kraftigt stigande-tröskel måste vara högre än stigande-tröskel",
"invalid_trend_strongly_falling_greater_than_falling": "Kraftigt fallande-tröskel måste vara lägre (mer negativ) än fallande-tröskel"
}, },
"abort": { "abort": {
"entry_not_found": "Tibber-konfigurationspost hittades inte.", "entry_not_found": "Tibber-konfigurationspost hittades inte.",
@ -592,73 +600,91 @@
"price_trend_1h": { "price_trend_1h": {
"name": "Pristrend (1h)", "name": "Pristrend (1h)",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"price_trend_2h": { "price_trend_2h": {
"name": "Pristrend (2h)", "name": "Pristrend (2h)",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"price_trend_3h": { "price_trend_3h": {
"name": "Pristrend (3h)", "name": "Pristrend (3h)",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"price_trend_4h": { "price_trend_4h": {
"name": "Pristrend (4h)", "name": "Pristrend (4h)",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"price_trend_5h": { "price_trend_5h": {
"name": "Pristrend (5h)", "name": "Pristrend (5h)",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"price_trend_6h": { "price_trend_6h": {
"name": "Pristrend (6h)", "name": "Pristrend (6h)",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"price_trend_8h": { "price_trend_8h": {
"name": "Pristrend (8h)", "name": "Pristrend (8h)",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"price_trend_12h": { "price_trend_12h": {
"name": "Pristrend (12h)", "name": "Pristrend (12h)",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"current_price_trend": { "current_price_trend": {
"name": "Aktuell pristrend", "name": "Aktuell pristrend",
"state": { "state": {
"strongly_rising": "Kraftigt stigande",
"rising": "Stigande", "rising": "Stigande",
"stable": "Stabil",
"falling": "Fallande", "falling": "Fallande",
"stable": "Stabil" "strongly_falling": "Kraftigt fallande"
} }
}, },
"next_price_trend_change": { "next_price_trend_change": {

View file

@ -20,6 +20,12 @@ from custom_components.tibber_prices.const import (
PRICE_LEVEL_MAPPING, PRICE_LEVEL_MAPPING,
PRICE_LEVEL_NORMAL, PRICE_LEVEL_NORMAL,
PRICE_RATING_NORMAL, PRICE_RATING_NORMAL,
PRICE_TREND_FALLING,
PRICE_TREND_MAPPING,
PRICE_TREND_RISING,
PRICE_TREND_STABLE,
PRICE_TREND_STRONGLY_FALLING,
PRICE_TREND_STRONGLY_RISING,
VOLATILITY_HIGH, VOLATILITY_HIGH,
VOLATILITY_LOW, VOLATILITY_LOW,
VOLATILITY_MODERATE, VOLATILITY_MODERATE,
@ -1130,15 +1136,27 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
threshold_rising: float = 3.0, threshold_rising: float = 3.0,
threshold_falling: float = -3.0, threshold_falling: float = -3.0,
*, *,
threshold_strongly_rising: float = 6.0,
threshold_strongly_falling: float = -6.0,
volatility_adjustment: bool = True, volatility_adjustment: bool = True,
lookahead_intervals: int | None = None, lookahead_intervals: int | None = None,
all_intervals: list[dict[str, Any]] | None = None, all_intervals: list[dict[str, Any]] | None = None,
volatility_threshold_moderate: float = DEFAULT_VOLATILITY_THRESHOLD_MODERATE, volatility_threshold_moderate: float = DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
volatility_threshold_high: float = DEFAULT_VOLATILITY_THRESHOLD_HIGH, volatility_threshold_high: float = DEFAULT_VOLATILITY_THRESHOLD_HIGH,
) -> tuple[str, float]: ) -> tuple[str, float, int]:
""" """
Calculate price trend by comparing current price with future average. Calculate price trend by comparing current price with future average.
Uses a 5-level trend scale with integer values for automation comparisons:
- strongly_falling (-2): difference <= strongly_falling_threshold
- falling (-1): difference <= falling_threshold
- stable (0): difference between thresholds
- rising (+1): difference >= rising_threshold
- strongly_rising (+2): difference >= strongly_rising_threshold
The strong thresholds are independently configurable (not derived from base
thresholds), allowing fine-grained control over trend sensitivity.
Supports volatility-adaptive thresholds: when enabled, the effective threshold Supports volatility-adaptive thresholds: when enabled, the effective threshold
is adjusted based on price volatility in the lookahead period. This makes the is adjusted based on price volatility in the lookahead period. This makes the
trend detection more sensitive during stable periods and less noisy during trend detection more sensitive during stable periods and less noisy during
@ -1152,6 +1170,8 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
future_average: Average price of future intervals future_average: Average price of future intervals
threshold_rising: Base threshold for rising trend (%, positive, default 3%) threshold_rising: Base threshold for rising trend (%, positive, default 3%)
threshold_falling: Base threshold for falling trend (%, negative, default -3%) threshold_falling: Base threshold for falling trend (%, negative, default -3%)
threshold_strongly_rising: Threshold for strongly rising (%, positive, default 6%)
threshold_strongly_falling: Threshold for strongly falling (%, negative, default -6%)
volatility_adjustment: Enable volatility-adaptive thresholds (default True) volatility_adjustment: Enable volatility-adaptive thresholds (default True)
lookahead_intervals: Number of intervals in trend period for volatility calc lookahead_intervals: Number of intervals in trend period for volatility calc
all_intervals: Price intervals (today + tomorrow) for volatility calculation all_intervals: Price intervals (today + tomorrow) for volatility calculation
@ -1159,9 +1179,10 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
volatility_threshold_high: User-configured high volatility threshold (%) volatility_threshold_high: User-configured high volatility threshold (%)
Returns: Returns:
Tuple of (trend_state, difference_percentage) Tuple of (trend_state, difference_percentage, trend_value)
trend_state: "rising" | "falling" | "stable" trend_state: PRICE_TREND_* constant (e.g., "strongly_rising")
difference_percentage: % change from current to future ((future - current) / current * 100) difference_percentage: % change from current to future ((future - current) / current * 100)
trend_value: Integer value from -2 to +2 for automation comparisons
Note: Note:
Volatility adjustment factor: Volatility adjustment factor:
@ -1172,12 +1193,13 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
""" """
if current_interval_price == 0: if current_interval_price == 0:
# Avoid division by zero - return stable trend # Avoid division by zero - return stable trend
return "stable", 0.0 return PRICE_TREND_STABLE, 0.0, PRICE_TREND_MAPPING[PRICE_TREND_STABLE]
# Apply volatility adjustment if enabled and data available # Apply volatility adjustment if enabled and data available
effective_rising = threshold_rising effective_rising = threshold_rising
effective_falling = threshold_falling effective_falling = threshold_falling
volatility_factor = 1.0 effective_strongly_rising = threshold_strongly_rising
effective_strongly_falling = threshold_strongly_falling
if volatility_adjustment and lookahead_intervals and all_intervals: if volatility_adjustment and lookahead_intervals and all_intervals:
volatility_factor = _calculate_lookahead_volatility_factor( volatility_factor = _calculate_lookahead_volatility_factor(
@ -1185,22 +1207,25 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
) )
effective_rising = threshold_rising * volatility_factor effective_rising = threshold_rising * volatility_factor
effective_falling = threshold_falling * volatility_factor effective_falling = threshold_falling * volatility_factor
effective_strongly_rising = threshold_strongly_rising * volatility_factor
effective_strongly_falling = threshold_strongly_falling * volatility_factor
# Calculate percentage difference from current to future # Calculate percentage difference from current to future
# CRITICAL: Use abs() for negative prices to get correct percentage direction # CRITICAL: Use abs() for negative prices to get correct percentage direction
# Example: current=-10, future=-5 → diff=5, pct=5/abs(-10)*100=+50% (correctly shows rising) # Example: current=-10, future=-5 → diff=5, pct=5/abs(-10)*100=+50% (correctly shows rising)
if current_interval_price == 0: diff_pct = ((future_average - current_interval_price) / abs(current_interval_price)) * 100
# Edge case: avoid division by zero
diff_pct = 0.0
else:
diff_pct = ((future_average - current_interval_price) / abs(current_interval_price)) * 100
# Determine trend based on effective thresholds # Determine trend based on effective thresholds (5-level scale)
if diff_pct >= effective_rising: # Check "strongly" conditions first (more extreme), then regular conditions
trend = "rising" if diff_pct >= effective_strongly_rising:
trend = PRICE_TREND_STRONGLY_RISING
elif diff_pct >= effective_rising:
trend = PRICE_TREND_RISING
elif diff_pct <= effective_strongly_falling:
trend = PRICE_TREND_STRONGLY_FALLING
elif diff_pct <= effective_falling: elif diff_pct <= effective_falling:
trend = "falling" trend = PRICE_TREND_FALLING
else: else:
trend = "stable" trend = PRICE_TREND_STABLE
return trend, diff_pct return trend, diff_pct, PRICE_TREND_MAPPING[trend]

View file

@ -99,20 +99,26 @@ def test_bug10_trend_diff_negative_current_price() -> None:
future_average = -0.05 future_average = -0.05
threshold_rising = 10.0 threshold_rising = 10.0
threshold_falling = -10.0 threshold_falling = -10.0
threshold_strongly_rising = 20.0
threshold_strongly_falling = -20.0
trend, diff_pct = calculate_price_trend( trend, diff_pct, trend_value = calculate_price_trend(
current_interval_price=current_interval_price, current_interval_price=current_interval_price,
future_average=future_average, future_average=future_average,
threshold_rising=threshold_rising, threshold_rising=threshold_rising,
threshold_falling=threshold_falling, threshold_falling=threshold_falling,
threshold_strongly_rising=threshold_strongly_rising,
threshold_strongly_falling=threshold_strongly_falling,
volatility_adjustment=False, # Disable to simplify test volatility_adjustment=False, # Disable to simplify test
) )
# Difference: -5 - (-10) = 5 ct # Difference: -5 - (-10) = 5 ct
# Percentage: 5 / abs(-10) * 100 = +50% (correctly shows rising) # Percentage: 5 / abs(-10) * 100 = +50% (correctly shows rising)
# With 5-level scale: +50% >= 20% strongly_rising threshold => strongly_rising
assert diff_pct > 0, "Percentage should be positive (price rising toward zero)" assert diff_pct > 0, "Percentage should be positive (price rising toward zero)"
assert diff_pct == pytest.approx(50.0, abs=0.1), "Should be +50%" assert diff_pct == pytest.approx(50.0, abs=0.1), "Should be +50%"
assert trend == "rising", "Trend should be 'rising' (above 10% threshold)" assert trend == "strongly_rising", "Trend should be 'strongly_rising' (above strongly_rising threshold)"
assert trend_value == 2, "Trend value should be 2 for strongly_rising"
def test_bug10_trend_diff_negative_falling_deeper() -> None: def test_bug10_trend_diff_negative_falling_deeper() -> None:
@ -126,20 +132,26 @@ def test_bug10_trend_diff_negative_falling_deeper() -> None:
future_average = -0.15 # -15 ct (more negative = cheaper) future_average = -0.15 # -15 ct (more negative = cheaper)
threshold_rising = 10.0 threshold_rising = 10.0
threshold_falling = -10.0 threshold_falling = -10.0
threshold_strongly_rising = 20.0
threshold_strongly_falling = -20.0
trend, diff_pct = calculate_price_trend( trend, diff_pct, trend_value = calculate_price_trend(
current_interval_price=current_interval_price, current_interval_price=current_interval_price,
future_average=future_average, future_average=future_average,
threshold_rising=threshold_rising, threshold_rising=threshold_rising,
threshold_falling=threshold_falling, threshold_falling=threshold_falling,
threshold_strongly_rising=threshold_strongly_rising,
threshold_strongly_falling=threshold_strongly_falling,
volatility_adjustment=False, volatility_adjustment=False,
) )
# Difference: -15 - (-10) = -5 ct # Difference: -15 - (-10) = -5 ct
# Percentage: -5 / abs(-10) * 100 = -50% (correctly shows falling) # Percentage: -5 / abs(-10) * 100 = -50% (correctly shows falling)
# With 5-level scale: -50% <= -20% strongly_falling threshold => strongly_falling
assert diff_pct < 0, "Percentage should be negative (price falling deeper)" assert diff_pct < 0, "Percentage should be negative (price falling deeper)"
assert diff_pct == pytest.approx(-50.0, abs=0.1), "Should be -50%" assert diff_pct == pytest.approx(-50.0, abs=0.1), "Should be -50%"
assert trend == "falling", "Trend should be 'falling' (below -10% threshold)" assert trend == "strongly_falling", "Trend should be 'strongly_falling' (below strongly_falling threshold)"
assert trend_value == -2, "Trend value should be -2 for strongly_falling"
def test_bug10_trend_diff_zero_current_price() -> None: def test_bug10_trend_diff_zero_current_price() -> None:
@ -152,18 +164,23 @@ def test_bug10_trend_diff_zero_current_price() -> None:
future_average = 0.05 future_average = 0.05
threshold_rising = 10.0 threshold_rising = 10.0
threshold_falling = -10.0 threshold_falling = -10.0
threshold_strongly_rising = 20.0
threshold_strongly_falling = -20.0
trend, diff_pct = calculate_price_trend( trend, diff_pct, trend_value = calculate_price_trend(
current_interval_price=current_interval_price, current_interval_price=current_interval_price,
future_average=future_average, future_average=future_average,
threshold_rising=threshold_rising, threshold_rising=threshold_rising,
threshold_falling=threshold_falling, threshold_falling=threshold_falling,
threshold_strongly_rising=threshold_strongly_rising,
threshold_strongly_falling=threshold_strongly_falling,
volatility_adjustment=False, volatility_adjustment=False,
) )
# Edge case: current=0 → diff_pct should be 0.0 (avoid division by zero) # Edge case: current=0 → diff_pct should be 0.0 (avoid division by zero)
assert diff_pct == 0.0, "Should return 0.0 to avoid division by zero" assert diff_pct == 0.0, "Should return 0.0 to avoid division by zero"
assert trend == "stable", "Should be stable when diff is 0%" assert trend == "stable", "Should be stable when diff is 0%"
assert trend_value == 0, "Trend value should be 0 for stable"
def test_bug10_trend_diff_positive_prices_unchanged() -> None: def test_bug10_trend_diff_positive_prices_unchanged() -> None:
@ -176,19 +193,25 @@ def test_bug10_trend_diff_positive_prices_unchanged() -> None:
future_average = 0.15 # 15 ct (rising) future_average = 0.15 # 15 ct (rising)
threshold_rising = 10.0 threshold_rising = 10.0
threshold_falling = -10.0 threshold_falling = -10.0
threshold_strongly_rising = 20.0
threshold_strongly_falling = -20.0
trend, diff_pct = calculate_price_trend( trend, diff_pct, trend_value = calculate_price_trend(
current_interval_price=current_interval_price, current_interval_price=current_interval_price,
future_average=future_average, future_average=future_average,
threshold_rising=threshold_rising, threshold_rising=threshold_rising,
threshold_falling=threshold_falling, threshold_falling=threshold_falling,
threshold_strongly_rising=threshold_strongly_rising,
threshold_strongly_falling=threshold_strongly_falling,
volatility_adjustment=False, volatility_adjustment=False,
) )
# Difference: 15 - 10 = 5 ct # Difference: 15 - 10 = 5 ct
# Percentage: 5 / 10 * 100 = +50% # Percentage: 5 / 10 * 100 = +50%
# With 5-level scale: +50% >= 20% strongly_rising threshold => strongly_rising
assert diff_pct == pytest.approx(50.0, abs=0.1), "Should be +50%" assert diff_pct == pytest.approx(50.0, abs=0.1), "Should be +50%"
assert trend == "rising", "Should be rising" assert trend == "strongly_rising", "Should be strongly_rising (above strongly_rising threshold)"
assert trend_value == 2, "Trend value should be 2 for strongly_rising"
def test_bug11_later_half_diff_calculation_note() -> None: def test_bug11_later_half_diff_calculation_note() -> None: