mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
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:
parent
972cbce1d3
commit
5fc1f4db33
15 changed files with 510 additions and 137 deletions
|
|
@ -33,6 +33,8 @@ from custom_components.tibber_prices.config_flow_handlers.validators import (
|
|||
validate_price_rating_thresholds,
|
||||
validate_price_trend_falling,
|
||||
validate_price_trend_rising,
|
||||
validate_price_trend_strongly_falling,
|
||||
validate_price_trend_strongly_rising,
|
||||
validate_relaxation_attempts,
|
||||
validate_volatility_threshold_high,
|
||||
validate_volatility_threshold_moderate,
|
||||
|
|
@ -54,6 +56,8 @@ from custom_components.tibber_prices.const import (
|
|||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
|
||||
CONF_RELAXATION_ATTEMPTS_BEST,
|
||||
CONF_RELAXATION_ATTEMPTS_PEAK,
|
||||
CONF_VOLATILITY_THRESHOLD_HIGH,
|
||||
|
|
@ -493,6 +497,34 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
):
|
||||
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:
|
||||
# Store flat data directly in options (no section wrapping)
|
||||
self._options.update(user_input)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ from custom_components.tibber_prices.const import (
|
|||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
|
||||
CONF_RELAXATION_ATTEMPTS_BEST,
|
||||
CONF_RELAXATION_ATTEMPTS_PEAK,
|
||||
CONF_VIRTUAL_TIME_OFFSET_DAYS,
|
||||
|
|
@ -66,6 +68,8 @@ from custom_components.tibber_prices.const import (
|
|||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
|
||||
DEFAULT_RELAXATION_ATTEMPTS_BEST,
|
||||
DEFAULT_RELAXATION_ATTEMPTS_PEAK,
|
||||
DEFAULT_VIRTUAL_TIME_OFFSET_DAYS,
|
||||
|
|
@ -86,6 +90,8 @@ from custom_components.tibber_prices.const import (
|
|||
MAX_PRICE_RATING_THRESHOLD_LOW,
|
||||
MAX_PRICE_TREND_FALLING,
|
||||
MAX_PRICE_TREND_RISING,
|
||||
MAX_PRICE_TREND_STRONGLY_FALLING,
|
||||
MAX_PRICE_TREND_STRONGLY_RISING,
|
||||
MAX_RELAXATION_ATTEMPTS,
|
||||
MAX_VOLATILITY_THRESHOLD_HIGH,
|
||||
MAX_VOLATILITY_THRESHOLD_MODERATE,
|
||||
|
|
@ -99,6 +105,8 @@ from custom_components.tibber_prices.const import (
|
|||
MIN_PRICE_RATING_THRESHOLD_LOW,
|
||||
MIN_PRICE_TREND_FALLING,
|
||||
MIN_PRICE_TREND_RISING,
|
||||
MIN_PRICE_TREND_STRONGLY_FALLING,
|
||||
MIN_PRICE_TREND_STRONGLY_RISING,
|
||||
MIN_RELAXATION_ATTEMPTS,
|
||||
MIN_VOLATILITY_THRESHOLD_HIGH,
|
||||
MIN_VOLATILITY_THRESHOLD_MODERATE,
|
||||
|
|
@ -745,6 +753,23 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
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(
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
default=int(
|
||||
|
|
@ -762,6 +787,23 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
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,
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ from custom_components.tibber_prices.const import (
|
|||
MAX_PRICE_RATING_THRESHOLD_LOW,
|
||||
MAX_PRICE_TREND_FALLING,
|
||||
MAX_PRICE_TREND_RISING,
|
||||
MAX_PRICE_TREND_STRONGLY_FALLING,
|
||||
MAX_PRICE_TREND_STRONGLY_RISING,
|
||||
MAX_RELAXATION_ATTEMPTS,
|
||||
MAX_VOLATILITY_THRESHOLD_HIGH,
|
||||
MAX_VOLATILITY_THRESHOLD_MODERATE,
|
||||
|
|
@ -30,6 +32,8 @@ from custom_components.tibber_prices.const import (
|
|||
MIN_PRICE_RATING_THRESHOLD_LOW,
|
||||
MIN_PRICE_TREND_FALLING,
|
||||
MIN_PRICE_TREND_RISING,
|
||||
MIN_PRICE_TREND_STRONGLY_FALLING,
|
||||
MIN_PRICE_TREND_STRONGLY_RISING,
|
||||
MIN_RELAXATION_ATTEMPTS,
|
||||
MIN_VOLATILITY_THRESHOLD_HIGH,
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ CONF_PRICE_LEVEL_GAP_TOLERANCE = "price_level_gap_tolerance"
|
|||
CONF_AVERAGE_SENSOR_DISPLAY = "average_sensor_display" # "median" or "mean"
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING = "price_trend_threshold_rising"
|
||||
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_HIGH = "volatility_threshold_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_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)
|
||||
# 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)
|
||||
# Coefficient of variation = (standard_deviation / mean) * 100%
|
||||
# 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
|
||||
MIN_PRICE_TREND_FALLING = -50 # Minimum 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
|
||||
MIN_GAP_COUNT = 0 # Minimum gap count
|
||||
|
|
@ -447,6 +458,14 @@ VOLATILITY_MODERATE = "MODERATE"
|
|||
VOLATILITY_HIGH = "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)
|
||||
# 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
|
||||
|
|
@ -472,6 +491,15 @@ VOLATILITY_OPTIONS = [
|
|||
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
|
||||
# Sorted from cheap to expensive: user selects "up to how expensive"
|
||||
BEST_PRICE_MAX_LEVEL_OPTIONS = [
|
||||
|
|
@ -514,6 +542,16 @@ PRICE_RATING_MAPPING = {
|
|||
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)
|
||||
PRICE_LEVEL_ICON_MAPPING = {
|
||||
PRICE_LEVEL_VERY_CHEAP: "mdi:gauge-empty",
|
||||
|
|
|
|||
|
|
@ -85,19 +85,25 @@ def get_dynamic_icon(
|
|||
|
||||
|
||||
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
|
||||
# (icon based on attributes, not value which is a timestamp)
|
||||
if key == "next_price_trend_change":
|
||||
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
|
||||
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
|
||||
# 5-level trend icons: strongly uses double arrows, normal uses single
|
||||
trend_icons = {
|
||||
"rising": "mdi:trending-up",
|
||||
"falling": "mdi:trending-down",
|
||||
"stable": "mdi:trending-neutral",
|
||||
"strongly_rising": "mdi:chevron-double-up", # Strong upward movement
|
||||
"rising": "mdi:trending-up", # Normal upward trend
|
||||
"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)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
# Get configured thresholds from options
|
||||
threshold_rising = self.config.get("price_trend_threshold_rising", 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_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)
|
||||
|
||||
# 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,
|
||||
future_mean,
|
||||
threshold_rising=threshold_rising,
|
||||
threshold_falling=threshold_falling,
|
||||
threshold_strongly_rising=threshold_strongly_rising,
|
||||
threshold_strongly_falling=threshold_strongly_falling,
|
||||
volatility_adjustment=True, # Always enabled
|
||||
lookahead_intervals=lookahead_intervals,
|
||||
all_intervals=all_intervals,
|
||||
|
|
@ -127,11 +131,14 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
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 = {
|
||||
"rising": "var(--error-color)", # Red/Orange for rising prices (expensive)
|
||||
"falling": "var(--success-color)", # Green for falling prices (cheaper)
|
||||
"strongly_rising": "var(--error-color)", # Red for strongly rising (very expensive)
|
||||
"rising": "var(--warning-color)", # Orange/Yellow for rising 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)")
|
||||
|
||||
# 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
|
||||
self._trend_attributes = {
|
||||
"timestamp": next_interval_start,
|
||||
"trend_value": trend_value,
|
||||
f"trend_{hours}h_%": round(diff_pct, 1),
|
||||
f"next_{hours}h_avg": round(future_mean * factor, 2),
|
||||
"interval_count": lookahead_intervals,
|
||||
|
|
@ -414,6 +422,8 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
return {
|
||||
"rising": self.config.get("price_trend_threshold_rising", 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),
|
||||
"high": self.config.get("volatility_threshold_high", 30.0),
|
||||
}
|
||||
|
|
@ -428,7 +438,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
current_index: Index of current interval
|
||||
|
||||
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
|
||||
|
|
@ -451,15 +461,25 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
weighted_sum = sum(price * weight for price, weight in zip(trailing_prices, weights, strict=True))
|
||||
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
|
||||
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:
|
||||
return "rising"
|
||||
if diff < -momentum_threshold:
|
||||
return "falling"
|
||||
return "stable"
|
||||
# Determine momentum level based on thresholds
|
||||
if diff >= strong_momentum_threshold:
|
||||
momentum = "strongly_rising"
|
||||
elif diff > momentum_threshold:
|
||||
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(
|
||||
self,
|
||||
|
|
@ -472,43 +492,60 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
"""
|
||||
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:
|
||||
current_momentum: Current momentum direction (rising/falling/stable)
|
||||
current_momentum: Current momentum direction (5-level scale)
|
||||
current_price: Current interval price
|
||||
future_mean: Average price in future window
|
||||
context: Dict with all_intervals, current_index, lookahead_intervals, thresholds
|
||||
|
||||
Returns:
|
||||
Final trend direction: "rising", "falling", or "stable"
|
||||
Final trend direction (5-level scale)
|
||||
|
||||
"""
|
||||
if current_momentum == "rising":
|
||||
# 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?
|
||||
# Use calculate_price_trend for consistency with 5-level logic
|
||||
all_intervals = context["all_intervals"]
|
||||
current_index = context["current_index"]
|
||||
lookahead_intervals = context["lookahead_intervals"]
|
||||
thresholds = context["thresholds"]
|
||||
|
||||
lookahead_for_volatility = all_intervals[current_index : current_index + lookahead_intervals]
|
||||
trend_state, _ = calculate_price_trend(
|
||||
future_trend, _, _ = calculate_price_trend(
|
||||
current_price,
|
||||
future_mean,
|
||||
threshold_rising=thresholds["rising"],
|
||||
threshold_falling=thresholds["falling"],
|
||||
threshold_strongly_rising=thresholds["strongly_rising"],
|
||||
threshold_strongly_falling=thresholds["strongly_falling"],
|
||||
volatility_adjustment=True,
|
||||
lookahead_intervals=lookahead_intervals,
|
||||
all_intervals=lookahead_for_volatility,
|
||||
volatility_threshold_moderate=thresholds["moderate"],
|
||||
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(
|
||||
self,
|
||||
|
|
@ -534,11 +571,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
current_price = float(current_interval["total"])
|
||||
|
||||
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,
|
||||
standard_future_mean,
|
||||
threshold_rising=thresholds["rising"],
|
||||
threshold_falling=thresholds["falling"],
|
||||
threshold_strongly_rising=thresholds["strongly_rising"],
|
||||
threshold_strongly_falling=thresholds["strongly_falling"],
|
||||
volatility_adjustment=True,
|
||||
lookahead_intervals=standard_lookahead,
|
||||
all_intervals=standard_lookahead_volatility,
|
||||
|
|
@ -606,11 +645,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
|
||||
# Calculate trend at this past point
|
||||
lookahead_for_volatility = all_intervals[i : i + intervals_in_3h]
|
||||
trend_state, _ = calculate_price_trend(
|
||||
trend_state, _, _ = calculate_price_trend(
|
||||
price,
|
||||
future_mean,
|
||||
threshold_rising=thresholds["rising"],
|
||||
threshold_falling=thresholds["falling"],
|
||||
threshold_strongly_rising=thresholds["strongly_rising"],
|
||||
threshold_strongly_falling=thresholds["strongly_falling"],
|
||||
volatility_adjustment=True,
|
||||
lookahead_intervals=intervals_in_3h,
|
||||
all_intervals=lookahead_for_volatility,
|
||||
|
|
@ -678,11 +719,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
|
|||
|
||||
# Calculate trend at this future point
|
||||
lookahead_for_volatility = all_intervals[i : i + intervals_in_3h]
|
||||
trend_state, _ = calculate_price_trend(
|
||||
trend_state, _, _ = calculate_price_trend(
|
||||
current_price,
|
||||
future_mean,
|
||||
threshold_rising=thresholds["rising"],
|
||||
threshold_falling=thresholds["falling"],
|
||||
threshold_strongly_rising=thresholds["strongly_rising"],
|
||||
threshold_strongly_falling=thresholds["strongly_falling"],
|
||||
volatility_adjustment=True,
|
||||
lookahead_intervals=intervals_in_3h,
|
||||
all_intervals=lookahead_for_volatility,
|
||||
|
|
|
|||
|
|
@ -987,11 +987,13 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
key = self.entity_description.key
|
||||
value = self.native_value
|
||||
|
||||
# Icon mapping for trend directions
|
||||
# Icon mapping for trend directions (5-level scale)
|
||||
trend_icons = {
|
||||
"strongly_rising": "mdi:chevron-double-up",
|
||||
"rising": "mdi:trending-up",
|
||||
"falling": "mdi:trending-down",
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ FUTURE_TREND_SENSORS = (
|
|||
icon="mdi:trending-up", # Dynamic: trending-up/trending-down/trending-neutral based on current trend
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
# 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
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
|
|
@ -580,7 +580,7 @@ FUTURE_TREND_SENSORS = (
|
|||
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=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
|
|
@ -590,7 +590,7 @@ FUTURE_TREND_SENSORS = (
|
|||
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=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
|
|
@ -600,7 +600,7 @@ FUTURE_TREND_SENSORS = (
|
|||
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=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
|
|
@ -610,7 +610,7 @@ FUTURE_TREND_SENSORS = (
|
|||
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=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
# 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
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None, # Enum values: no statistics
|
||||
options=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
|
|
@ -631,7 +631,7 @@ FUTURE_TREND_SENSORS = (
|
|||
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=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
|
|
@ -641,7 +641,7 @@ FUTURE_TREND_SENSORS = (
|
|||
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=["rising", "falling", "stable"],
|
||||
options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -283,14 +283,18 @@
|
|||
},
|
||||
"price_trend": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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_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_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_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"
|
||||
},
|
||||
|
|
@ -356,7 +360,11 @@
|
|||
"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_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": {
|
||||
"entry_not_found": "Tibber Konfigurationseintrag nicht gefunden.",
|
||||
|
|
@ -592,73 +600,91 @@
|
|||
"price_trend_1h": {
|
||||
"name": "Preistrend (1h)",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"price_trend_2h": {
|
||||
"name": "Preistrend (2h)",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"price_trend_3h": {
|
||||
"name": "Preistrend (3h)",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"price_trend_4h": {
|
||||
"name": "Preistrend (4h)",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"price_trend_5h": {
|
||||
"name": "Preistrend (5h)",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"price_trend_6h": {
|
||||
"name": "Preistrend (6h)",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"price_trend_8h": {
|
||||
"name": "Preistrend (8h)",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"price_trend_12h": {
|
||||
"name": "Preistrend (12h)",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"current_price_trend": {
|
||||
"name": "Aktueller Preistrend",
|
||||
"state": {
|
||||
"strongly_rising": "Stark steigend",
|
||||
"rising": "Steigend",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallend",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Stark fallend"
|
||||
}
|
||||
},
|
||||
"next_price_trend_change": {
|
||||
|
|
|
|||
|
|
@ -294,14 +294,18 @@
|
|||
},
|
||||
"price_trend": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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_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_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_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"
|
||||
},
|
||||
|
|
@ -356,7 +360,11 @@
|
|||
"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_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": {
|
||||
"entry_not_found": "Tibber configuration entry not found.",
|
||||
|
|
@ -592,73 +600,91 @@
|
|||
"price_trend_1h": {
|
||||
"name": "Price Trend (1h)",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"price_trend_2h": {
|
||||
"name": "Price Trend (2h)",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"price_trend_3h": {
|
||||
"name": "Price Trend (3h)",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"price_trend_4h": {
|
||||
"name": "Price Trend (4h)",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"price_trend_5h": {
|
||||
"name": "Price Trend (5h)",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"price_trend_6h": {
|
||||
"name": "Price Trend (6h)",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"price_trend_8h": {
|
||||
"name": "Price Trend (8h)",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"price_trend_12h": {
|
||||
"name": "Price Trend (12h)",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"current_price_trend": {
|
||||
"name": "Current Price Trend",
|
||||
"state": {
|
||||
"strongly_rising": "Strongly Rising",
|
||||
"rising": "Rising",
|
||||
"stable": "Stable",
|
||||
"falling": "Falling",
|
||||
"stable": "Stable"
|
||||
"strongly_falling": "Strongly Falling"
|
||||
}
|
||||
},
|
||||
"next_price_trend_change": {
|
||||
|
|
|
|||
|
|
@ -283,14 +283,18 @@
|
|||
},
|
||||
"price_trend": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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_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_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_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"
|
||||
},
|
||||
|
|
@ -356,7 +360,11 @@
|
|||
"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_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": {
|
||||
"entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet.",
|
||||
|
|
@ -592,73 +600,91 @@
|
|||
"price_trend_1h": {
|
||||
"name": "Pristrend (1t)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"price_trend_2h": {
|
||||
"name": "Pristrend (2t)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"price_trend_3h": {
|
||||
"name": "Pristrend (3t)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"price_trend_4h": {
|
||||
"name": "Pristrend (4t)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"price_trend_5h": {
|
||||
"name": "Pristrend (5t)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"price_trend_6h": {
|
||||
"name": "Pristrend (6t)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"price_trend_8h": {
|
||||
"name": "Pristrend (8t)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"price_trend_12h": {
|
||||
"name": "Pristrend (12t)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"current_price_trend": {
|
||||
"name": "Nåværende pristrend",
|
||||
"state": {
|
||||
"strongly_rising": "Sterkt stigende",
|
||||
"rising": "Stigende",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallende",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Sterkt fallende"
|
||||
}
|
||||
},
|
||||
"next_price_trend_change": {
|
||||
|
|
|
|||
|
|
@ -283,14 +283,18 @@
|
|||
},
|
||||
"price_trend": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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_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_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_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"
|
||||
},
|
||||
|
|
@ -356,7 +360,11 @@
|
|||
"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_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": {
|
||||
"entry_not_found": "Tibber-configuratie-item niet gevonden.",
|
||||
|
|
@ -592,73 +600,91 @@
|
|||
"price_trend_1h": {
|
||||
"name": "Prijstrend (1u)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"price_trend_2h": {
|
||||
"name": "Prijstrend (2u)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"price_trend_3h": {
|
||||
"name": "Prijstrend (3u)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"price_trend_4h": {
|
||||
"name": "Prijstrend (4u)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"price_trend_5h": {
|
||||
"name": "Prijstrend (5u)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"price_trend_6h": {
|
||||
"name": "Prijstrend (6u)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"price_trend_8h": {
|
||||
"name": "Prijstrend (8u)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"price_trend_12h": {
|
||||
"name": "Prijstrend (12u)",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"current_price_trend": {
|
||||
"name": "Huidige Prijstrend",
|
||||
"state": {
|
||||
"strongly_rising": "Sterk stijgend",
|
||||
"rising": "Stijgend",
|
||||
"stable": "Stabiel",
|
||||
"falling": "Dalend",
|
||||
"stable": "Stabiel"
|
||||
"strongly_falling": "Sterk dalend"
|
||||
}
|
||||
},
|
||||
"next_price_trend_change": {
|
||||
|
|
|
|||
|
|
@ -283,14 +283,18 @@
|
|||
},
|
||||
"price_trend": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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_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_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_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"
|
||||
},
|
||||
|
|
@ -356,7 +360,11 @@
|
|||
"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_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": {
|
||||
"entry_not_found": "Tibber-konfigurationspost hittades inte.",
|
||||
|
|
@ -592,73 +600,91 @@
|
|||
"price_trend_1h": {
|
||||
"name": "Pristrend (1h)",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"price_trend_2h": {
|
||||
"name": "Pristrend (2h)",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"price_trend_3h": {
|
||||
"name": "Pristrend (3h)",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"price_trend_4h": {
|
||||
"name": "Pristrend (4h)",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"price_trend_5h": {
|
||||
"name": "Pristrend (5h)",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"price_trend_6h": {
|
||||
"name": "Pristrend (6h)",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"price_trend_8h": {
|
||||
"name": "Pristrend (8h)",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"price_trend_12h": {
|
||||
"name": "Pristrend (12h)",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"current_price_trend": {
|
||||
"name": "Aktuell pristrend",
|
||||
"state": {
|
||||
"strongly_rising": "Kraftigt stigande",
|
||||
"rising": "Stigande",
|
||||
"stable": "Stabil",
|
||||
"falling": "Fallande",
|
||||
"stable": "Stabil"
|
||||
"strongly_falling": "Kraftigt fallande"
|
||||
}
|
||||
},
|
||||
"next_price_trend_change": {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ from custom_components.tibber_prices.const import (
|
|||
PRICE_LEVEL_MAPPING,
|
||||
PRICE_LEVEL_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_LOW,
|
||||
VOLATILITY_MODERATE,
|
||||
|
|
@ -1130,15 +1136,27 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
|
|||
threshold_rising: float = 3.0,
|
||||
threshold_falling: float = -3.0,
|
||||
*,
|
||||
threshold_strongly_rising: float = 6.0,
|
||||
threshold_strongly_falling: float = -6.0,
|
||||
volatility_adjustment: bool = True,
|
||||
lookahead_intervals: int | None = None,
|
||||
all_intervals: list[dict[str, Any]] | None = None,
|
||||
volatility_threshold_moderate: float = DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
||||
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.
|
||||
|
||||
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
|
||||
is adjusted based on price volatility in the lookahead period. This makes the
|
||||
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
|
||||
threshold_rising: Base threshold for rising trend (%, positive, 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)
|
||||
lookahead_intervals: Number of intervals in trend period for volatility calc
|
||||
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 (%)
|
||||
|
||||
Returns:
|
||||
Tuple of (trend_state, difference_percentage)
|
||||
trend_state: "rising" | "falling" | "stable"
|
||||
Tuple of (trend_state, difference_percentage, trend_value)
|
||||
trend_state: PRICE_TREND_* constant (e.g., "strongly_rising")
|
||||
difference_percentage: % change from current to future ((future - current) / current * 100)
|
||||
trend_value: Integer value from -2 to +2 for automation comparisons
|
||||
|
||||
Note:
|
||||
Volatility adjustment factor:
|
||||
|
|
@ -1172,12 +1193,13 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
|
|||
"""
|
||||
if current_interval_price == 0:
|
||||
# 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
|
||||
effective_rising = threshold_rising
|
||||
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:
|
||||
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_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
|
||||
# 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)
|
||||
if current_interval_price == 0:
|
||||
# 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
|
||||
if diff_pct >= effective_rising:
|
||||
trend = "rising"
|
||||
# Determine trend based on effective thresholds (5-level scale)
|
||||
# Check "strongly" conditions first (more extreme), then regular conditions
|
||||
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:
|
||||
trend = "falling"
|
||||
trend = PRICE_TREND_FALLING
|
||||
else:
|
||||
trend = "stable"
|
||||
trend = PRICE_TREND_STABLE
|
||||
|
||||
return trend, diff_pct
|
||||
return trend, diff_pct, PRICE_TREND_MAPPING[trend]
|
||||
|
|
|
|||
|
|
@ -99,20 +99,26 @@ def test_bug10_trend_diff_negative_current_price() -> None:
|
|||
future_average = -0.05
|
||||
threshold_rising = 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,
|
||||
future_average=future_average,
|
||||
threshold_rising=threshold_rising,
|
||||
threshold_falling=threshold_falling,
|
||||
threshold_strongly_rising=threshold_strongly_rising,
|
||||
threshold_strongly_falling=threshold_strongly_falling,
|
||||
volatility_adjustment=False, # Disable to simplify test
|
||||
)
|
||||
|
||||
# Difference: -5 - (-10) = 5 ct
|
||||
# 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 == 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:
|
||||
|
|
@ -126,20 +132,26 @@ def test_bug10_trend_diff_negative_falling_deeper() -> None:
|
|||
future_average = -0.15 # -15 ct (more negative = cheaper)
|
||||
threshold_rising = 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,
|
||||
future_average=future_average,
|
||||
threshold_rising=threshold_rising,
|
||||
threshold_falling=threshold_falling,
|
||||
threshold_strongly_rising=threshold_strongly_rising,
|
||||
threshold_strongly_falling=threshold_strongly_falling,
|
||||
volatility_adjustment=False,
|
||||
)
|
||||
|
||||
# Difference: -15 - (-10) = -5 ct
|
||||
# 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 == 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:
|
||||
|
|
@ -152,18 +164,23 @@ def test_bug10_trend_diff_zero_current_price() -> None:
|
|||
future_average = 0.05
|
||||
threshold_rising = 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,
|
||||
future_average=future_average,
|
||||
threshold_rising=threshold_rising,
|
||||
threshold_falling=threshold_falling,
|
||||
threshold_strongly_rising=threshold_strongly_rising,
|
||||
threshold_strongly_falling=threshold_strongly_falling,
|
||||
volatility_adjustment=False,
|
||||
)
|
||||
|
||||
# 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 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:
|
||||
|
|
@ -176,19 +193,25 @@ def test_bug10_trend_diff_positive_prices_unchanged() -> None:
|
|||
future_average = 0.15 # 15 ct (rising)
|
||||
threshold_rising = 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,
|
||||
future_average=future_average,
|
||||
threshold_rising=threshold_rising,
|
||||
threshold_falling=threshold_falling,
|
||||
threshold_strongly_rising=threshold_strongly_rising,
|
||||
threshold_strongly_falling=threshold_strongly_falling,
|
||||
volatility_adjustment=False,
|
||||
)
|
||||
|
||||
# Difference: 15 - 10 = 5 ct
|
||||
# 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 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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue