mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 13:23:41 +00:00
chore: Enhance validation logic and constants for options configuration flow
- Added new validation functions for various parameters including flexibility percentage, distance percentage, minimum periods, gap count, relaxation attempts, price rating thresholds, volatility threshold, and price trend thresholds. - Updated constants in `const.py` to define maximum and minimum limits for the new validation criteria. - Improved error messages in translations for invalid parameters to provide clearer guidance to users. - Adjusted existing validation functions to ensure they align with the new constants and validation logic.
This commit is contained in:
parent
db3268e54d
commit
ebd1b8ddbf
9 changed files with 571 additions and 87 deletions
|
|
@ -3,7 +3,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, ClassVar
|
from typing import TYPE_CHECKING, Any, ClassVar
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
||||||
get_best_price_schema,
|
get_best_price_schema,
|
||||||
|
|
@ -14,7 +17,42 @@ from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
||||||
get_price_trend_schema,
|
get_price_trend_schema,
|
||||||
get_volatility_schema,
|
get_volatility_schema,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
from custom_components.tibber_prices.config_flow_handlers.validators import (
|
||||||
|
validate_distance_percentage,
|
||||||
|
validate_flex_percentage,
|
||||||
|
validate_gap_count,
|
||||||
|
validate_min_periods,
|
||||||
|
validate_period_length,
|
||||||
|
validate_price_rating_threshold_high,
|
||||||
|
validate_price_rating_threshold_low,
|
||||||
|
validate_price_rating_thresholds,
|
||||||
|
validate_price_trend_falling,
|
||||||
|
validate_price_trend_rising,
|
||||||
|
validate_relaxation_attempts,
|
||||||
|
validate_volatility_threshold,
|
||||||
|
)
|
||||||
|
from custom_components.tibber_prices.const import (
|
||||||
|
CONF_BEST_PRICE_FLEX,
|
||||||
|
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
|
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||||
|
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
|
||||||
|
CONF_MIN_PERIODS_BEST,
|
||||||
|
CONF_MIN_PERIODS_PEAK,
|
||||||
|
CONF_PEAK_PRICE_FLEX,
|
||||||
|
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
|
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||||
|
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
||||||
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||||
|
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||||
|
CONF_RELAXATION_ATTEMPTS_BEST,
|
||||||
|
CONF_RELAXATION_ATTEMPTS_PEAK,
|
||||||
|
CONF_VOLATILITY_THRESHOLD_HIGH,
|
||||||
|
CONF_VOLATILITY_THRESHOLD_MODERATE,
|
||||||
|
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
|
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@ -39,6 +77,64 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
"""Initialize options flow."""
|
"""Initialize options flow."""
|
||||||
self._options: dict[str, Any] = {}
|
self._options: dict[str, Any] = {}
|
||||||
|
|
||||||
|
def _migrate_config_options(self, options: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Migrate deprecated config options to current format.
|
||||||
|
|
||||||
|
This removes obsolete keys and renames changed keys to maintain
|
||||||
|
compatibility with older config entries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options: Original options dict from config_entry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Migrated options dict with deprecated keys removed/renamed
|
||||||
|
|
||||||
|
"""
|
||||||
|
migrated = dict(options)
|
||||||
|
migration_performed = False
|
||||||
|
|
||||||
|
# Migration 1: Rename relaxation_step_* to relaxation_attempts_*
|
||||||
|
# (Changed in v0.6.0 - commit 5a5c8ca)
|
||||||
|
if "relaxation_step_best" in migrated:
|
||||||
|
migrated["relaxation_attempts_best"] = migrated.pop("relaxation_step_best")
|
||||||
|
migration_performed = True
|
||||||
|
_LOGGER.info(
|
||||||
|
"Migrated config option: relaxation_step_best -> relaxation_attempts_best (value: %s)",
|
||||||
|
migrated["relaxation_attempts_best"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if "relaxation_step_peak" in migrated:
|
||||||
|
migrated["relaxation_attempts_peak"] = migrated.pop("relaxation_step_peak")
|
||||||
|
migration_performed = True
|
||||||
|
_LOGGER.info(
|
||||||
|
"Migrated config option: relaxation_step_peak -> relaxation_attempts_peak (value: %s)",
|
||||||
|
migrated["relaxation_attempts_peak"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Migration 2: Remove obsolete volatility filter options
|
||||||
|
# (Removed in v0.9.0 - volatility filter feature removed)
|
||||||
|
obsolete_keys = [
|
||||||
|
"best_price_min_volatility",
|
||||||
|
"peak_price_min_volatility",
|
||||||
|
"min_volatility_for_periods",
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in obsolete_keys:
|
||||||
|
if key in migrated:
|
||||||
|
old_value = migrated.pop(key)
|
||||||
|
migration_performed = True
|
||||||
|
_LOGGER.info(
|
||||||
|
"Removed obsolete config option: %s (was: %s)",
|
||||||
|
key,
|
||||||
|
old_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if migration_performed:
|
||||||
|
_LOGGER.info("Config migration completed - deprecated options cleaned up")
|
||||||
|
|
||||||
|
return migrated
|
||||||
|
|
||||||
def _get_step_description_placeholders(self, step_id: str) -> dict[str, str]:
|
def _get_step_description_placeholders(self, step_id: str) -> dict[str, str]:
|
||||||
"""Get description placeholders with step progress."""
|
"""Get description placeholders with step progress."""
|
||||||
if step_id not in self._STEP_INFO:
|
if step_id not in self._STEP_INFO:
|
||||||
|
|
@ -62,7 +158,8 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
"""Manage the options - General Settings."""
|
"""Manage the options - General Settings."""
|
||||||
# Initialize options from config_entry on first call
|
# Initialize options from config_entry on first call
|
||||||
if not self._options:
|
if not self._options:
|
||||||
self._options = dict(self.config_entry.options)
|
# Migrate deprecated config options before processing
|
||||||
|
self._options = self._migrate_config_options(self.config_entry.options)
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self._options.update(user_input)
|
self._options.update(user_input)
|
||||||
|
|
@ -81,7 +178,34 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Configure price rating thresholds."""
|
"""Configure price rating thresholds."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
# Validate low price rating threshold
|
||||||
|
if CONF_PRICE_RATING_THRESHOLD_LOW in user_input and not validate_price_rating_threshold_low(
|
||||||
|
user_input[CONF_PRICE_RATING_THRESHOLD_LOW]
|
||||||
|
):
|
||||||
|
errors[CONF_PRICE_RATING_THRESHOLD_LOW] = "invalid_price_rating_low"
|
||||||
|
|
||||||
|
# Validate high price rating threshold
|
||||||
|
if CONF_PRICE_RATING_THRESHOLD_HIGH in user_input and not validate_price_rating_threshold_high(
|
||||||
|
user_input[CONF_PRICE_RATING_THRESHOLD_HIGH]
|
||||||
|
):
|
||||||
|
errors[CONF_PRICE_RATING_THRESHOLD_HIGH] = "invalid_price_rating_high"
|
||||||
|
|
||||||
|
# Cross-validate both thresholds together (LOW must be < HIGH)
|
||||||
|
if not errors and not validate_price_rating_thresholds(
|
||||||
|
user_input.get(
|
||||||
|
CONF_PRICE_RATING_THRESHOLD_LOW, self._options.get(CONF_PRICE_RATING_THRESHOLD_LOW, -10)
|
||||||
|
),
|
||||||
|
user_input.get(
|
||||||
|
CONF_PRICE_RATING_THRESHOLD_HIGH, self._options.get(CONF_PRICE_RATING_THRESHOLD_HIGH, 10)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
# This should never happen given the range constraints, but add error for safety
|
||||||
|
errors["base"] = "invalid_price_rating_thresholds"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
self._options.update(user_input)
|
self._options.update(user_input)
|
||||||
return await self.async_step_volatility()
|
return await self.async_step_volatility()
|
||||||
|
|
||||||
|
|
@ -89,11 +213,47 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
step_id="current_interval_price_rating",
|
step_id="current_interval_price_rating",
|
||||||
data_schema=get_price_rating_schema(self.config_entry.options),
|
data_schema=get_price_rating_schema(self.config_entry.options),
|
||||||
description_placeholders=self._get_step_description_placeholders("current_interval_price_rating"),
|
description_placeholders=self._get_step_description_placeholders("current_interval_price_rating"),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_best_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
async def async_step_best_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||||
"""Configure best price period settings."""
|
"""Configure best price period settings."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
# Validate period length
|
||||||
|
if CONF_BEST_PRICE_MIN_PERIOD_LENGTH in user_input and not validate_period_length(
|
||||||
|
user_input[CONF_BEST_PRICE_MIN_PERIOD_LENGTH]
|
||||||
|
):
|
||||||
|
errors[CONF_BEST_PRICE_MIN_PERIOD_LENGTH] = "invalid_period_length"
|
||||||
|
|
||||||
|
# Validate flex percentage
|
||||||
|
if CONF_BEST_PRICE_FLEX in user_input and not validate_flex_percentage(user_input[CONF_BEST_PRICE_FLEX]):
|
||||||
|
errors[CONF_BEST_PRICE_FLEX] = "invalid_flex"
|
||||||
|
|
||||||
|
# Validate distance from average
|
||||||
|
if CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG in user_input and not validate_distance_percentage(
|
||||||
|
user_input[CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG]
|
||||||
|
):
|
||||||
|
errors[CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG] = "invalid_distance"
|
||||||
|
|
||||||
|
# Validate minimum periods count
|
||||||
|
if CONF_MIN_PERIODS_BEST in user_input and not validate_min_periods(user_input[CONF_MIN_PERIODS_BEST]):
|
||||||
|
errors[CONF_MIN_PERIODS_BEST] = "invalid_min_periods"
|
||||||
|
|
||||||
|
# Validate gap count
|
||||||
|
if CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT in user_input and not validate_gap_count(
|
||||||
|
user_input[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT]
|
||||||
|
):
|
||||||
|
errors[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT] = "invalid_gap_count"
|
||||||
|
|
||||||
|
# Validate relaxation attempts
|
||||||
|
if CONF_RELAXATION_ATTEMPTS_BEST in user_input and not validate_relaxation_attempts(
|
||||||
|
user_input[CONF_RELAXATION_ATTEMPTS_BEST]
|
||||||
|
):
|
||||||
|
errors[CONF_RELAXATION_ATTEMPTS_BEST] = "invalid_relaxation_attempts"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
self._options.update(user_input)
|
self._options.update(user_input)
|
||||||
return await self.async_step_peak_price()
|
return await self.async_step_peak_price()
|
||||||
|
|
||||||
|
|
@ -101,11 +261,47 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
step_id="best_price",
|
step_id="best_price",
|
||||||
data_schema=get_best_price_schema(self.config_entry.options),
|
data_schema=get_best_price_schema(self.config_entry.options),
|
||||||
description_placeholders=self._get_step_description_placeholders("best_price"),
|
description_placeholders=self._get_step_description_placeholders("best_price"),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_peak_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
async def async_step_peak_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||||
"""Configure peak price period settings."""
|
"""Configure peak price period settings."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
# Validate period length
|
||||||
|
if CONF_PEAK_PRICE_MIN_PERIOD_LENGTH in user_input and not validate_period_length(
|
||||||
|
user_input[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH]
|
||||||
|
):
|
||||||
|
errors[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH] = "invalid_period_length"
|
||||||
|
|
||||||
|
# Validate flex percentage (peak uses negative values)
|
||||||
|
if CONF_PEAK_PRICE_FLEX in user_input and not validate_flex_percentage(user_input[CONF_PEAK_PRICE_FLEX]):
|
||||||
|
errors[CONF_PEAK_PRICE_FLEX] = "invalid_flex"
|
||||||
|
|
||||||
|
# Validate distance from average
|
||||||
|
if CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG in user_input and not validate_distance_percentage(
|
||||||
|
user_input[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG]
|
||||||
|
):
|
||||||
|
errors[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG] = "invalid_distance"
|
||||||
|
|
||||||
|
# Validate minimum periods count
|
||||||
|
if CONF_MIN_PERIODS_PEAK in user_input and not validate_min_periods(user_input[CONF_MIN_PERIODS_PEAK]):
|
||||||
|
errors[CONF_MIN_PERIODS_PEAK] = "invalid_min_periods"
|
||||||
|
|
||||||
|
# Validate gap count
|
||||||
|
if CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT in user_input and not validate_gap_count(
|
||||||
|
user_input[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT]
|
||||||
|
):
|
||||||
|
errors[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT] = "invalid_gap_count"
|
||||||
|
|
||||||
|
# Validate relaxation attempts
|
||||||
|
if CONF_RELAXATION_ATTEMPTS_PEAK in user_input and not validate_relaxation_attempts(
|
||||||
|
user_input[CONF_RELAXATION_ATTEMPTS_PEAK]
|
||||||
|
):
|
||||||
|
errors[CONF_RELAXATION_ATTEMPTS_PEAK] = "invalid_relaxation_attempts"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
self._options.update(user_input)
|
self._options.update(user_input)
|
||||||
return await self.async_step_price_trend()
|
return await self.async_step_price_trend()
|
||||||
|
|
||||||
|
|
@ -113,11 +309,27 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
step_id="peak_price",
|
step_id="peak_price",
|
||||||
data_schema=get_peak_price_schema(self.config_entry.options),
|
data_schema=get_peak_price_schema(self.config_entry.options),
|
||||||
description_placeholders=self._get_step_description_placeholders("peak_price"),
|
description_placeholders=self._get_step_description_placeholders("peak_price"),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_price_trend(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
async def async_step_price_trend(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||||
"""Configure price trend thresholds."""
|
"""Configure price trend thresholds."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
# Validate rising trend threshold
|
||||||
|
if CONF_PRICE_TREND_THRESHOLD_RISING in user_input and not validate_price_trend_rising(
|
||||||
|
user_input[CONF_PRICE_TREND_THRESHOLD_RISING]
|
||||||
|
):
|
||||||
|
errors[CONF_PRICE_TREND_THRESHOLD_RISING] = "invalid_price_trend_rising"
|
||||||
|
|
||||||
|
# Validate falling trend threshold
|
||||||
|
if CONF_PRICE_TREND_THRESHOLD_FALLING in user_input and not validate_price_trend_falling(
|
||||||
|
user_input[CONF_PRICE_TREND_THRESHOLD_FALLING]
|
||||||
|
):
|
||||||
|
errors[CONF_PRICE_TREND_THRESHOLD_FALLING] = "invalid_price_trend_falling"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
self._options.update(user_input)
|
self._options.update(user_input)
|
||||||
return await self.async_step_chart_data_export()
|
return await self.async_step_chart_data_export()
|
||||||
|
|
||||||
|
|
@ -125,6 +337,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
step_id="price_trend",
|
step_id="price_trend",
|
||||||
data_schema=get_price_trend_schema(self.config_entry.options),
|
data_schema=get_price_trend_schema(self.config_entry.options),
|
||||||
description_placeholders=self._get_step_description_placeholders("price_trend"),
|
description_placeholders=self._get_step_description_placeholders("price_trend"),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_chart_data_export(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
async def async_step_chart_data_export(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||||
|
|
@ -142,7 +355,28 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
|
|
||||||
async def async_step_volatility(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
async def async_step_volatility(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||||
"""Configure volatility thresholds and period filtering."""
|
"""Configure volatility thresholds and period filtering."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
# Validate moderate volatility threshold
|
||||||
|
if CONF_VOLATILITY_THRESHOLD_MODERATE in user_input and not validate_volatility_threshold(
|
||||||
|
user_input[CONF_VOLATILITY_THRESHOLD_MODERATE]
|
||||||
|
):
|
||||||
|
errors[CONF_VOLATILITY_THRESHOLD_MODERATE] = "invalid_volatility_threshold"
|
||||||
|
|
||||||
|
# Validate high volatility threshold
|
||||||
|
if CONF_VOLATILITY_THRESHOLD_HIGH in user_input and not validate_volatility_threshold(
|
||||||
|
user_input[CONF_VOLATILITY_THRESHOLD_HIGH]
|
||||||
|
):
|
||||||
|
errors[CONF_VOLATILITY_THRESHOLD_HIGH] = "invalid_volatility_threshold"
|
||||||
|
|
||||||
|
# Validate very high volatility threshold
|
||||||
|
if CONF_VOLATILITY_THRESHOLD_VERY_HIGH in user_input and not validate_volatility_threshold(
|
||||||
|
user_input[CONF_VOLATILITY_THRESHOLD_VERY_HIGH]
|
||||||
|
):
|
||||||
|
errors[CONF_VOLATILITY_THRESHOLD_VERY_HIGH] = "invalid_volatility_threshold"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
self._options.update(user_input)
|
self._options.update(user_input)
|
||||||
return await self.async_step_best_price()
|
return await self.async_step_best_price()
|
||||||
|
|
||||||
|
|
@ -150,4 +384,5 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
step_id="volatility",
|
step_id="volatility",
|
||||||
data_schema=get_volatility_schema(self.config_entry.options),
|
data_schema=get_volatility_schema(self.config_entry.options),
|
||||||
description_placeholders=self._get_step_description_placeholders("volatility"),
|
description_placeholders=self._get_step_description_placeholders("volatility"),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,23 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||||
|
MAX_GAP_COUNT,
|
||||||
|
MAX_MIN_PERIOD_LENGTH,
|
||||||
|
MAX_MIN_PERIODS,
|
||||||
|
MAX_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
MAX_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
MAX_PRICE_TREND_FALLING,
|
||||||
|
MAX_PRICE_TREND_RISING,
|
||||||
|
MAX_RELAXATION_ATTEMPTS,
|
||||||
|
MAX_VOLATILITY_THRESHOLD,
|
||||||
|
MIN_GAP_COUNT,
|
||||||
|
MIN_PERIOD_LENGTH,
|
||||||
|
MIN_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
MIN_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
MIN_PRICE_TREND_FALLING,
|
||||||
|
MIN_PRICE_TREND_RISING,
|
||||||
|
MIN_RELAXATION_ATTEMPTS,
|
||||||
|
MIN_VOLATILITY_THRESHOLD,
|
||||||
PEAK_PRICE_MIN_LEVEL_OPTIONS,
|
PEAK_PRICE_MIN_LEVEL_OPTIONS,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
@ -156,8 +173,8 @@ def get_price_rating_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=-100,
|
min=MIN_PRICE_RATING_THRESHOLD_LOW,
|
||||||
max=0,
|
max=MAX_PRICE_RATING_THRESHOLD_LOW,
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%",
|
||||||
step=1,
|
step=1,
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
|
@ -173,8 +190,8 @@ def get_price_rating_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=0,
|
min=MIN_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
max=100,
|
max=MAX_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%",
|
||||||
step=1,
|
step=1,
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
|
@ -198,8 +215,8 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=0.0,
|
min=MIN_VOLATILITY_THRESHOLD,
|
||||||
max=100.0,
|
max=MAX_VOLATILITY_THRESHOLD,
|
||||||
step=0.1,
|
step=0.1,
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%",
|
||||||
mode=NumberSelectorMode.BOX,
|
mode=NumberSelectorMode.BOX,
|
||||||
|
|
@ -215,8 +232,8 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=0.0,
|
min=MIN_VOLATILITY_THRESHOLD,
|
||||||
max=100.0,
|
max=MAX_VOLATILITY_THRESHOLD,
|
||||||
step=0.1,
|
step=0.1,
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%",
|
||||||
mode=NumberSelectorMode.BOX,
|
mode=NumberSelectorMode.BOX,
|
||||||
|
|
@ -232,8 +249,8 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=0.0,
|
min=MIN_VOLATILITY_THRESHOLD,
|
||||||
max=100.0,
|
max=MAX_VOLATILITY_THRESHOLD,
|
||||||
step=0.1,
|
step=0.1,
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%",
|
||||||
mode=NumberSelectorMode.BOX,
|
mode=NumberSelectorMode.BOX,
|
||||||
|
|
@ -257,8 +274,8 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=15,
|
min=MIN_PERIOD_LENGTH,
|
||||||
max=240,
|
max=MAX_MIN_PERIOD_LENGTH,
|
||||||
step=15,
|
step=15,
|
||||||
unit_of_measurement="min",
|
unit_of_measurement="min",
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
|
@ -321,8 +338,8 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=0,
|
min=MIN_GAP_COUNT,
|
||||||
max=8,
|
max=MAX_GAP_COUNT,
|
||||||
step=1,
|
step=1,
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
),
|
),
|
||||||
|
|
@ -345,7 +362,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=1,
|
min=1,
|
||||||
max=10,
|
max=MAX_MIN_PERIODS,
|
||||||
step=1,
|
step=1,
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
),
|
),
|
||||||
|
|
@ -360,8 +377,8 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=1,
|
min=MIN_RELAXATION_ATTEMPTS,
|
||||||
max=12,
|
max=MAX_RELAXATION_ATTEMPTS,
|
||||||
step=1,
|
step=1,
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
),
|
),
|
||||||
|
|
@ -384,8 +401,8 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=15,
|
min=MIN_PERIOD_LENGTH,
|
||||||
max=240,
|
max=MAX_MIN_PERIOD_LENGTH,
|
||||||
step=15,
|
step=15,
|
||||||
unit_of_measurement="min",
|
unit_of_measurement="min",
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
|
@ -448,8 +465,8 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=0,
|
min=MIN_GAP_COUNT,
|
||||||
max=8,
|
max=MAX_GAP_COUNT,
|
||||||
step=1,
|
step=1,
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
),
|
),
|
||||||
|
|
@ -472,7 +489,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=1,
|
min=1,
|
||||||
max=10,
|
max=MAX_MIN_PERIODS,
|
||||||
step=1,
|
step=1,
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
),
|
),
|
||||||
|
|
@ -487,8 +504,8 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=1,
|
min=MIN_RELAXATION_ATTEMPTS,
|
||||||
max=12,
|
max=MAX_RELAXATION_ATTEMPTS,
|
||||||
step=1,
|
step=1,
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
),
|
),
|
||||||
|
|
@ -511,8 +528,8 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=1,
|
min=MIN_PRICE_TREND_RISING,
|
||||||
max=50,
|
max=MAX_PRICE_TREND_RISING,
|
||||||
step=1,
|
step=1,
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%",
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
|
@ -528,8 +545,8 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
),
|
),
|
||||||
): NumberSelector(
|
): NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=-50,
|
min=MIN_PRICE_TREND_FALLING,
|
||||||
max=-1,
|
max=MAX_PRICE_TREND_FALLING,
|
||||||
step=1,
|
step=1,
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%",
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,27 @@ from custom_components.tibber_prices.api import (
|
||||||
TibberPricesApiClientCommunicationError,
|
TibberPricesApiClientCommunicationError,
|
||||||
TibberPricesApiClientError,
|
TibberPricesApiClientError,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
from custom_components.tibber_prices.const import (
|
||||||
|
DOMAIN,
|
||||||
|
MAX_DISTANCE_PERCENTAGE,
|
||||||
|
MAX_FLEX_PERCENTAGE,
|
||||||
|
MAX_GAP_COUNT,
|
||||||
|
MAX_MIN_PERIODS,
|
||||||
|
MAX_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
MAX_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
MAX_PRICE_TREND_FALLING,
|
||||||
|
MAX_PRICE_TREND_RISING,
|
||||||
|
MAX_RELAXATION_ATTEMPTS,
|
||||||
|
MAX_VOLATILITY_THRESHOLD,
|
||||||
|
MIN_GAP_COUNT,
|
||||||
|
MIN_PERIOD_LENGTH,
|
||||||
|
MIN_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
MIN_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
MIN_PRICE_TREND_FALLING,
|
||||||
|
MIN_PRICE_TREND_RISING,
|
||||||
|
MIN_RELAXATION_ATTEMPTS,
|
||||||
|
MIN_VOLATILITY_THRESHOLD,
|
||||||
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
from homeassistant.loader import async_get_integration
|
from homeassistant.loader import async_get_integration
|
||||||
|
|
@ -18,10 +38,6 @@ from homeassistant.loader import async_get_integration
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
# Constants for validation
|
|
||||||
MAX_FLEX_PERCENTAGE = 100.0
|
|
||||||
MAX_MIN_PERIODS = 10 # Arbitrary upper limit for sanity
|
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesInvalidAuthError(HomeAssistantError):
|
class TibberPricesInvalidAuthError(HomeAssistantError):
|
||||||
"""Error to indicate invalid authentication."""
|
"""Error to indicate invalid authentication."""
|
||||||
|
|
@ -64,34 +80,18 @@ async def validate_api_token(hass: HomeAssistant, token: str) -> dict:
|
||||||
raise TibberPricesCannotConnectError from exception
|
raise TibberPricesCannotConnectError from exception
|
||||||
|
|
||||||
|
|
||||||
def validate_threshold_range(value: float, min_val: float, max_val: float) -> bool:
|
|
||||||
"""
|
|
||||||
Validate threshold is within allowed range.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: Value to validate
|
|
||||||
min_val: Minimum allowed value
|
|
||||||
max_val: Maximum allowed value
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if value is within range
|
|
||||||
|
|
||||||
"""
|
|
||||||
return min_val <= value <= max_val
|
|
||||||
|
|
||||||
|
|
||||||
def validate_period_length(minutes: int) -> bool:
|
def validate_period_length(minutes: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Validate period length is multiple of 15 minutes.
|
Validate period length is a positive multiple of 15 minutes.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
minutes: Period length in minutes
|
minutes: Period length in minutes
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if length is valid
|
True if length is valid (multiple of 15, at least MIN_PERIOD_LENGTH)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return minutes > 0 and minutes % 15 == 0
|
return minutes % 15 == 0 and minutes >= MIN_PERIOD_LENGTH
|
||||||
|
|
||||||
|
|
||||||
def validate_flex_percentage(flex: float) -> bool:
|
def validate_flex_percentage(flex: float) -> bool:
|
||||||
|
|
@ -99,13 +99,13 @@ def validate_flex_percentage(flex: float) -> bool:
|
||||||
Validate flexibility percentage is within bounds.
|
Validate flexibility percentage is within bounds.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flex: Flexibility percentage
|
flex: Flexibility percentage (can be negative for peak price)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if percentage is valid
|
True if percentage is valid (-MAX_FLEX to +MAX_FLEX)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return 0.0 <= flex <= MAX_FLEX_PERCENTAGE
|
return -MAX_FLEX_PERCENTAGE <= flex <= MAX_FLEX_PERCENTAGE
|
||||||
|
|
||||||
|
|
||||||
def validate_min_periods(count: int) -> bool:
|
def validate_min_periods(count: int) -> bool:
|
||||||
|
|
@ -113,10 +113,149 @@ def validate_min_periods(count: int) -> bool:
|
||||||
Validate minimum periods count is reasonable.
|
Validate minimum periods count is reasonable.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
count: Number of minimum periods
|
count: Number of minimum periods per day
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if count is valid
|
True if count is valid (1 to MAX_MIN_PERIODS)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return count > 0 and count <= MAX_MIN_PERIODS
|
return 1 <= count <= MAX_MIN_PERIODS
|
||||||
|
|
||||||
|
|
||||||
|
def validate_distance_percentage(distance: float) -> bool:
|
||||||
|
"""
|
||||||
|
Validate distance from average percentage is reasonable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance: Distance percentage (0-50% is typical range)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if distance is valid (0-MAX_DISTANCE_PERCENTAGE)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return 0.0 <= distance <= MAX_DISTANCE_PERCENTAGE
|
||||||
|
|
||||||
|
|
||||||
|
def validate_gap_count(count: int) -> bool:
|
||||||
|
"""
|
||||||
|
Validate gap count is within bounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: Gap count (0-8)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if count is valid (MIN_GAP_COUNT to MAX_GAP_COUNT)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return MIN_GAP_COUNT <= count <= MAX_GAP_COUNT
|
||||||
|
|
||||||
|
|
||||||
|
def validate_relaxation_attempts(attempts: int) -> bool:
|
||||||
|
"""
|
||||||
|
Validate relaxation attempts count is within bounds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attempts: Number of relaxation attempts (1-12)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if attempts is valid (MIN_RELAXATION_ATTEMPTS to MAX_RELAXATION_ATTEMPTS)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return MIN_RELAXATION_ATTEMPTS <= attempts <= MAX_RELAXATION_ATTEMPTS
|
||||||
|
|
||||||
|
|
||||||
|
def validate_price_rating_threshold_low(threshold: int) -> bool:
|
||||||
|
"""
|
||||||
|
Validate low price rating threshold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
threshold: Low rating threshold percentage (-50 to -5)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if threshold is valid (MIN_PRICE_RATING_THRESHOLD_LOW to MAX_PRICE_RATING_THRESHOLD_LOW)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return MIN_PRICE_RATING_THRESHOLD_LOW <= threshold <= MAX_PRICE_RATING_THRESHOLD_LOW
|
||||||
|
|
||||||
|
|
||||||
|
def validate_price_rating_threshold_high(threshold: int) -> bool:
|
||||||
|
"""
|
||||||
|
Validate high price rating threshold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
threshold: High rating threshold percentage (5 to 50)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if threshold is valid (MIN_PRICE_RATING_THRESHOLD_HIGH to MAX_PRICE_RATING_THRESHOLD_HIGH)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return MIN_PRICE_RATING_THRESHOLD_HIGH <= threshold <= MAX_PRICE_RATING_THRESHOLD_HIGH
|
||||||
|
|
||||||
|
|
||||||
|
def validate_price_rating_thresholds(threshold_low: int, threshold_high: int) -> bool:
|
||||||
|
"""
|
||||||
|
Cross-validate both price rating thresholds together.
|
||||||
|
|
||||||
|
Ensures that LOW threshold < HIGH threshold with proper gap to avoid
|
||||||
|
overlap at 0%. LOW should be negative (below average), HIGH should be
|
||||||
|
positive (above average).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
threshold_low: Low rating threshold percentage (-50 to -5)
|
||||||
|
threshold_high: High rating threshold percentage (5 to 50)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if both thresholds are valid individually AND threshold_low < threshold_high
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Validate individual ranges first
|
||||||
|
if not validate_price_rating_threshold_low(threshold_low):
|
||||||
|
return False
|
||||||
|
if not validate_price_rating_threshold_high(threshold_high):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ensure LOW is always less than HIGH (should always be true given the ranges,
|
||||||
|
# but explicit check for safety)
|
||||||
|
return threshold_low < threshold_high
|
||||||
|
|
||||||
|
|
||||||
|
def validate_volatility_threshold(threshold: float) -> bool:
|
||||||
|
"""
|
||||||
|
Validate volatility threshold percentage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
threshold: Volatility threshold percentage (0.0 to 100.0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if threshold is valid (MIN_VOLATILITY_THRESHOLD to MAX_VOLATILITY_THRESHOLD)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return MIN_VOLATILITY_THRESHOLD <= threshold <= MAX_VOLATILITY_THRESHOLD
|
||||||
|
|
||||||
|
|
||||||
|
def validate_price_trend_rising(threshold: int) -> bool:
|
||||||
|
"""
|
||||||
|
Validate rising price trend threshold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
threshold: Rising trend threshold percentage (1 to 50)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if threshold is valid (MIN_PRICE_TREND_RISING to MAX_PRICE_TREND_RISING)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return MIN_PRICE_TREND_RISING <= threshold <= MAX_PRICE_TREND_RISING
|
||||||
|
|
||||||
|
|
||||||
|
def validate_price_trend_falling(threshold: int) -> bool:
|
||||||
|
"""
|
||||||
|
Validate falling price trend threshold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
threshold: Falling trend threshold percentage (-50 to -1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if threshold is valid (MIN_PRICE_TREND_FALLING to MAX_PRICE_TREND_FALLING)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return MIN_PRICE_TREND_FALLING <= threshold <= MAX_PRICE_TREND_FALLING
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,39 @@ DEFAULT_ENABLE_MIN_PERIODS_PEAK = True # Default: minimum periods feature enabl
|
||||||
DEFAULT_MIN_PERIODS_PEAK = 2 # Default: require at least 2 peak price periods (when enabled)
|
DEFAULT_MIN_PERIODS_PEAK = 2 # Default: require at least 2 peak price periods (when enabled)
|
||||||
DEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # Default: 11 steps allows escalation from 20% to 50% (3% increment per step)
|
DEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # Default: 11 steps allows escalation from 20% to 50% (3% increment per step)
|
||||||
|
|
||||||
|
# Validation limits (used in GUI schemas and server-side validation)
|
||||||
|
# These ensure consistency between frontend and backend validation
|
||||||
|
MAX_FLEX_PERCENTAGE = 50 # Maximum flexibility percentage (aligned with GUI slider and MAX_SAFE_FLEX)
|
||||||
|
MAX_DISTANCE_PERCENTAGE = 50 # Maximum distance from average percentage (GUI slider limit)
|
||||||
|
MAX_GAP_COUNT = 8 # Maximum gap count for level filtering (GUI slider limit)
|
||||||
|
MAX_MIN_PERIODS = 10 # Maximum number of minimum periods per day (GUI slider limit)
|
||||||
|
MAX_RELAXATION_ATTEMPTS = 12 # Maximum relaxation attempts (GUI slider limit)
|
||||||
|
MIN_PERIOD_LENGTH = 15 # Minimum period length in minutes (1 quarter hour)
|
||||||
|
MAX_MIN_PERIOD_LENGTH = 180 # Maximum for minimum period length setting (3 hours - realistic for required minimum)
|
||||||
|
|
||||||
|
# Price rating threshold limits
|
||||||
|
# LOW threshold: negative values (prices below average) - practical range -50% to -5%
|
||||||
|
# HIGH threshold: positive values (prices above average) - practical range +5% to +50%
|
||||||
|
# Ensure minimum 5% gap between thresholds to avoid overlap at 0%
|
||||||
|
MIN_PRICE_RATING_THRESHOLD_LOW = -50 # Minimum value for low rating threshold
|
||||||
|
MAX_PRICE_RATING_THRESHOLD_LOW = -5 # Maximum value for low rating threshold (must be < HIGH)
|
||||||
|
MIN_PRICE_RATING_THRESHOLD_HIGH = 5 # Minimum value for high rating threshold (must be > LOW)
|
||||||
|
MAX_PRICE_RATING_THRESHOLD_HIGH = 50 # Maximum value for high rating threshold
|
||||||
|
|
||||||
|
# Volatility threshold limits
|
||||||
|
MIN_VOLATILITY_THRESHOLD = 0.0 # Minimum volatility threshold percentage
|
||||||
|
MAX_VOLATILITY_THRESHOLD = 100.0 # Maximum volatility threshold percentage
|
||||||
|
|
||||||
|
# Price trend threshold limits
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Gap count and relaxation limits
|
||||||
|
MIN_GAP_COUNT = 0 # Minimum gap count
|
||||||
|
MIN_RELAXATION_ATTEMPTS = 1 # Minimum relaxation attempts
|
||||||
|
|
||||||
# Home types
|
# Home types
|
||||||
HOME_TYPE_APARTMENT = "APARTMENT"
|
HOME_TYPE_APARTMENT = "APARTMENT"
|
||||||
HOME_TYPE_ROWHOUSE = "ROWHOUSE"
|
HOME_TYPE_ROWHOUSE = "ROWHOUSE"
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
"best_price_flex": "Maximal über dem täglichen Mindestpreis, bei dem Intervalle noch als 'Bestpreis' qualifizieren. Empfehlung: 15-20 mit aktivierter Lockerung (Standard), oder 25-35 ohne Lockerung. Maximum: 50 (harte Grenze für zuverlässige Zeitraumerkennung).",
|
"best_price_flex": "Maximal über dem täglichen Mindestpreis, bei dem Intervalle noch als 'Bestpreis' qualifizieren. Empfehlung: 15-20 mit aktivierter Lockerung (Standard), oder 25-35 ohne Lockerung. Maximum: 50 (harte Grenze für zuverlässige Zeitraumerkennung).",
|
||||||
"best_price_min_distance_from_avg": "Stellt sicher, dass Zeiträume signifikant günstiger als der Tagesdurchschnitt sind, nicht nur geringfügig darunter. Dies filtert Rauschen und verhindert, dass leicht unterdurchschnittliche Zeiträume an Tagen mit flachen Preisen als 'Bestpreis' markiert werden. Höhere Werte = strengere Filterung (nur wirklich günstige Zeiträume qualifizieren). Standard: 5 bedeutet, Zeiträume müssen mindestens 5% unter dem Tagesdurchschnitt liegen.",
|
"best_price_min_distance_from_avg": "Stellt sicher, dass Zeiträume signifikant günstiger als der Tagesdurchschnitt sind, nicht nur geringfügig darunter. Dies filtert Rauschen und verhindert, dass leicht unterdurchschnittliche Zeiträume an Tagen mit flachen Preisen als 'Bestpreis' markiert werden. Höhere Werte = strengere Filterung (nur wirklich günstige Zeiträume qualifizieren). Standard: 5 bedeutet, Zeiträume müssen mindestens 5% unter dem Tagesdurchschnitt liegen.",
|
||||||
"best_price_max_level": "Zeigt Bestpreis-Zeiträume nur an, wenn sie Intervalle mit Preisniveaus ≤ dem gewählten Wert enthalten. Beispiel: Wahl von 'Günstig' bedeutet, dass der Zeitraum mindestens ein 'SEHR GÜNSTIG' oder 'GÜNSTIG' Intervall haben muss. Dies stellt sicher, dass Bestpreis-Zeiträume nicht nur relativ günstig für den Tag sind, sondern tatsächlich günstig in absoluten Zahlen. Wähle 'Beliebig' um Bestpreise unabhängig vom absoluten Preisniveau anzuzeigen.",
|
"best_price_max_level": "Zeigt Bestpreis-Zeiträume nur an, wenn sie Intervalle mit Preisniveaus ≤ dem gewählten Wert enthalten. Beispiel: Wahl von 'Günstig' bedeutet, dass der Zeitraum mindestens ein 'SEHR GÜNSTIG' oder 'GÜNSTIG' Intervall haben muss. Dies stellt sicher, dass Bestpreis-Zeiträume nicht nur relativ günstig für den Tag sind, sondern tatsächlich günstig in absoluten Zahlen. Wähle 'Beliebig' um Bestpreise unabhängig vom absoluten Preisniveau anzuzeigen.",
|
||||||
"best_price_max_level_gap_count": "Maximale Anzahl aufeinanderfolgender Intervalle, die exakt um eine Niveaustufe vom geforderten Level abweichen dürfen. Beispiel: Bei Filter 'Günstig' und Lückentoleranz 1 wird die Sequenz 'GÜNSTIG, GÜNSTIG, NORMAL, GÜNSTIG' akzeptiert (NORMAL ist eine Stufe über GÜNSTIG). Dies verhindert, dass Zeiträume durch gelegentliche Niveau-Abweichungen aufgespalten werden. Standard: 1.",
|
"best_price_max_level_gap_count": "Maximale Anzahl aufeinanderfolgender Intervalle, die exakt um eine Niveaustufe vom geforderten Level abweichen dürfen. Beispiel: Bei Filter 'Günstig' und Lückentoleranz 1 wird die Sequenz 'GÜNSTIG, GÜNSTIG, NORMAL, GÜNSTIG' akzeptiert (NORMAL ist eine Stufe über GÜNSTIG). Dies verhindert, dass Zeiträume durch gelegentliche Niveau-Abweichungen aufgespalten werden. **Hinweis:** Lückentoleranz erfordert Zeiträume ≥90 Minuten (6 Intervalle), um Ausreißer effektiv zu erkennen. Standard: 0 (strenge Filterung, keine Toleranz).",
|
||||||
"enable_min_periods_best": "Wenn aktiviert, werden Filter schrittweise gelockert, falls nicht genug Zeiträume gefunden wurden. Dies versucht die gewünschte Mindestanzahl zu erreichen, was dazu führen kann, dass auch weniger optimale Zeiträume als Bestpreis-Zeiträume markiert werden.",
|
"enable_min_periods_best": "Wenn aktiviert, werden Filter schrittweise gelockert, falls nicht genug Zeiträume gefunden wurden. Dies versucht die gewünschte Mindestanzahl zu erreichen, was dazu führen kann, dass auch weniger optimale Zeiträume als Bestpreis-Zeiträume markiert werden.",
|
||||||
"min_periods_best": "Mindestanzahl an Bestpreis-Zeiträumen, die pro Tag angestrebt werden. Filter werden schrittweise gelockert, um diese Anzahl zu erreichen. Nur aktiv, wenn 'Mindestanzahl Zeiträume anstreben' aktiviert ist. Standard: 1",
|
"min_periods_best": "Mindestanzahl an Bestpreis-Zeiträumen, die pro Tag angestrebt werden. Filter werden schrittweise gelockert, um diese Anzahl zu erreichen. Nur aktiv, wenn 'Mindestanzahl Zeiträume anstreben' aktiviert ist. Standard: 1",
|
||||||
"relaxation_attempts_best": "Wie viele Flex-Stufen (Versuche) nacheinander ausprobiert werden, bevor aufgegeben wird. Jeder Versuch testet alle Filterkombinationen auf der neuen Flex-Stufe. Mehr Versuche erhöhen die Chance auf zusätzliche Zeiträume, benötigen aber etwas mehr Rechenzeit."
|
"relaxation_attempts_best": "Wie viele Flex-Stufen (Versuche) nacheinander ausprobiert werden, bevor aufgegeben wird. Jeder Versuch testet alle Filterkombinationen auf der neuen Flex-Stufe. Mehr Versuche erhöhen die Chance auf zusätzliche Zeiträume, benötigen aber etwas mehr Rechenzeit."
|
||||||
|
|
@ -150,7 +150,7 @@
|
||||||
"peak_price_flex": "Maximal unter dem täglichen Höchstpreis, bei dem Intervalle noch als 'Spitzenpreis' qualifizieren. Empfehlung: -15 bis -20 mit aktivierter Lockerung (Standard), oder -25 bis -35 ohne Lockerung. Maximum: -50 (harte Grenze für zuverlässige Zeitraumerkennung). Hinweis: Negative Werte zeigen den Abstand unter dem Maximum an.",
|
"peak_price_flex": "Maximal unter dem täglichen Höchstpreis, bei dem Intervalle noch als 'Spitzenpreis' qualifizieren. Empfehlung: -15 bis -20 mit aktivierter Lockerung (Standard), oder -25 bis -35 ohne Lockerung. Maximum: -50 (harte Grenze für zuverlässige Zeitraumerkennung). Hinweis: Negative Werte zeigen den Abstand unter dem Maximum an.",
|
||||||
"peak_price_min_distance_from_avg": "Stellt sicher, dass Zeiträume signifikant teurer als der Tagesdurchschnitt sind, nicht nur geringfügig darüber. Dies filtert Rauschen und verhindert, dass leicht überdurchschnittliche Zeiträume an Tagen mit flachen Preisen als 'Spitzenpreis' markiert werden. Höhere Werte = strengere Filterung (nur wirklich teure Zeiträume qualifizieren). Standard: 5 bedeutet, Zeiträume müssen mindestens 5% über dem Tagesdurchschnitt liegen.",
|
"peak_price_min_distance_from_avg": "Stellt sicher, dass Zeiträume signifikant teurer als der Tagesdurchschnitt sind, nicht nur geringfügig darüber. Dies filtert Rauschen und verhindert, dass leicht überdurchschnittliche Zeiträume an Tagen mit flachen Preisen als 'Spitzenpreis' markiert werden. Höhere Werte = strengere Filterung (nur wirklich teure Zeiträume qualifizieren). Standard: 5 bedeutet, Zeiträume müssen mindestens 5% über dem Tagesdurchschnitt liegen.",
|
||||||
"peak_price_min_level": "Zeigt Spitzenpreis-Zeiträume nur an, wenn sie Intervalle mit Preisniveaus ≥ dem gewählten Wert enthalten. Beispiel: Wahl von 'Teuer' bedeutet, dass der Zeitraum mindestens ein 'TEUER' oder 'SEHR TEUER' Intervall haben muss. Dies stellt sicher, dass Spitzenpreis-Zeiträume nicht nur relativ teuer für den Tag sind, sondern tatsächlich teuer in absoluten Zahlen. Wähle 'Beliebig' um Spitzenpreise unabhängig vom absoluten Preisniveau anzuzeigen.",
|
"peak_price_min_level": "Zeigt Spitzenpreis-Zeiträume nur an, wenn sie Intervalle mit Preisniveaus ≥ dem gewählten Wert enthalten. Beispiel: Wahl von 'Teuer' bedeutet, dass der Zeitraum mindestens ein 'TEUER' oder 'SEHR TEUER' Intervall haben muss. Dies stellt sicher, dass Spitzenpreis-Zeiträume nicht nur relativ teuer für den Tag sind, sondern tatsächlich teuer in absoluten Zahlen. Wähle 'Beliebig' um Spitzenpreise unabhängig vom absoluten Preisniveau anzuzeigen.",
|
||||||
"peak_price_max_level_gap_count": "Maximale Anzahl aufeinanderfolgender Intervalle, die exakt um eine Niveaustufe vom geforderten Level abweichen dürfen. Beispiel: Bei Filter 'Teuer' und Lückentoleranz 2 wird die Sequenz 'TEUER, NORMAL, NORMAL, TEUER' akzeptiert (NORMAL ist eine Stufe unter TEUER). Dies verhindert, dass Zeiträume durch gelegentliche Niveau-Abweichungen aufgespalten werden. Standard: 0.",
|
"peak_price_max_level_gap_count": "Maximale Anzahl aufeinanderfolgender Intervalle, die exakt um eine Niveaustufe vom geforderten Level abweichen dürfen. Beispiel: Bei Filter 'Teuer' und Lückentoleranz 2 wird die Sequenz 'TEUER, NORMAL, NORMAL, TEUER' akzeptiert (NORMAL ist eine Stufe unter TEUER). Dies verhindert, dass Zeiträume durch gelegentliche Niveau-Abweichungen aufgespalten werden. **Hinweis:** Lückentoleranz erfordert Zeiträume ≥90 Minuten (6 Intervalle), um Ausreißer effektiv zu erkennen. Standard: 0 (strenge Filterung, keine Toleranz).",
|
||||||
"enable_min_periods_peak": "Wenn aktiviert, werden Filter schrittweise gelockert, falls nicht genug Zeiträume gefunden wurden. Dies versucht die gewünschte Mindestanzahl zu erreichen, um sicherzustellen, dass du auch an Tagen mit ungewöhnlichen Preismustern vor teuren Zeiträumen gewarnt wirst.",
|
"enable_min_periods_peak": "Wenn aktiviert, werden Filter schrittweise gelockert, falls nicht genug Zeiträume gefunden wurden. Dies versucht die gewünschte Mindestanzahl zu erreichen, um sicherzustellen, dass du auch an Tagen mit ungewöhnlichen Preismustern vor teuren Zeiträumen gewarnt wirst.",
|
||||||
"min_periods_peak": "Mindestanzahl an Spitzenpreis-Zeiträumen, die pro Tag angestrebt werden. Filter werden schrittweise gelockert, um diese Anzahl zu erreichen. Nur aktiv, wenn 'Mindestanzahl Zeiträume anstreben' aktiviert ist. Standard: 1",
|
"min_periods_peak": "Mindestanzahl an Spitzenpreis-Zeiträumen, die pro Tag angestrebt werden. Filter werden schrittweise gelockert, um diese Anzahl zu erreichen. Nur aktiv, wenn 'Mindestanzahl Zeiträume anstreben' aktiviert ist. Standard: 1",
|
||||||
"relaxation_attempts_peak": "Wie viele Flex-Stufen (Versuche) nacheinander ausprobiert werden, bevor aufgegeben wird. Jeder Versuch testet alle Filterkombinationen auf der neuen Flex-Stufe. Mehr Versuche erhöhen die Chance auf zusätzliche Spitzenpreis-Zeiträume, benötigen aber etwas mehr Rechenzeit."
|
"relaxation_attempts_peak": "Wie viele Flex-Stufen (Versuche) nacheinander ausprobiert werden, bevor aufgegeben wird. Jeder Versuch testet alle Filterkombinationen auf der neuen Flex-Stufe. Mehr Versuche erhöhen die Chance auf zusätzliche Spitzenpreis-Zeiträume, benötigen aber etwas mehr Rechenzeit."
|
||||||
|
|
@ -197,7 +197,19 @@
|
||||||
"unknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte überprüfe die Logs für Details.",
|
"unknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte überprüfe die Logs für Details.",
|
||||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||||
"invalid_access_token": "Ungültiges Zugriffstoken",
|
"invalid_access_token": "Ungültiges Zugriffstoken",
|
||||||
"different_home": "Der Zugriffstoken ist nicht gültig für die Home ID, für die diese Integration konfiguriert ist."
|
"different_home": "Der Zugriffstoken ist nicht gültig für die Home ID, für die diese Integration konfiguriert ist.",
|
||||||
|
"invalid_flex": "TRANSLATE: Flexibility percentage must be between -50% and +50%",
|
||||||
|
"invalid_distance": "TRANSLATE: Distance percentage must be between 0% and 50%",
|
||||||
|
"invalid_min_periods": "TRANSLATE: Minimum periods count must be between 1 and 10",
|
||||||
|
"invalid_period_length": "Die Periodenlänge muss mindestens 15 Minuten betragen (Vielfache von 15).",
|
||||||
|
"invalid_gap_count": "Lückentoleranz muss zwischen 0 und 8 liegen",
|
||||||
|
"invalid_relaxation_attempts": "Lockerungsversuche müssen zwischen 1 und 12 liegen",
|
||||||
|
"invalid_price_rating_low": "Untere Preis-Bewertungsschwelle muss zwischen -50% und -5% liegen",
|
||||||
|
"invalid_price_rating_high": "Obere Preis-Bewertungsschwelle muss zwischen 5% und 50% liegen",
|
||||||
|
"invalid_price_rating_thresholds": "Untere Schwelle muss kleiner als obere Schwelle sein",
|
||||||
|
"invalid_volatility_threshold": "Volatilitätsschwelle muss zwischen 0% und 100% 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"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"entry_not_found": "Tibber Konfigurationseintrag nicht gefunden."
|
"entry_not_found": "Tibber Konfigurationseintrag nicht gefunden."
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
"best_price_flex": "Maximum above the daily minimum price that intervals can be and still qualify as 'best price'. Recommended: 15-20 with relaxation enabled (default), or 25-35 without relaxation. Maximum: 50 (hard cap for reliable period detection).",
|
"best_price_flex": "Maximum above the daily minimum price that intervals can be and still qualify as 'best price'. Recommended: 15-20 with relaxation enabled (default), or 25-35 without relaxation. Maximum: 50 (hard cap for reliable period detection).",
|
||||||
"best_price_min_distance_from_avg": "Ensures periods are significantly cheaper than the daily average, not just marginally below it. This filters out noise and prevents marking slightly-below-average periods as 'best price' on days with flat prices. Higher values = stricter filtering (only truly cheap periods qualify). Default: 5 means periods must be at least 5% below the daily average.",
|
"best_price_min_distance_from_avg": "Ensures periods are significantly cheaper than the daily average, not just marginally below it. This filters out noise and prevents marking slightly-below-average periods as 'best price' on days with flat prices. Higher values = stricter filtering (only truly cheap periods qualify). Default: 5 means periods must be at least 5% below the daily average.",
|
||||||
"best_price_max_level": "Only show best price periods if they contain intervals with price levels ≤ selected value. For example, selecting 'Cheap' means the period must have at least one 'VERY_CHEAP' or 'CHEAP' interval. This ensures 'best price' periods are not just relatively cheap for the day, but actually cheap in absolute terms. Select 'Any' to show best prices regardless of their absolute price level.",
|
"best_price_max_level": "Only show best price periods if they contain intervals with price levels ≤ selected value. For example, selecting 'Cheap' means the period must have at least one 'VERY_CHEAP' or 'CHEAP' interval. This ensures 'best price' periods are not just relatively cheap for the day, but actually cheap in absolute terms. Select 'Any' to show best prices regardless of their absolute price level.",
|
||||||
"best_price_max_level_gap_count": "Maximum number of consecutive intervals allowed that deviate by exactly one level step from the required level. For example: with 'Cheap' filter and gap count 1, a sequence 'CHEAP, CHEAP, NORMAL, CHEAP' is accepted (NORMAL is one step above CHEAP). This prevents periods from being split by occasional level deviations. Default: 0 (strict filtering, no tolerance).",
|
"best_price_max_level_gap_count": "Maximum number of consecutive intervals allowed that deviate by exactly one level step from the required level. For example: with 'Cheap' filter and gap count 1, a sequence 'CHEAP, CHEAP, NORMAL, CHEAP' is accepted (NORMAL is one step above CHEAP). This prevents periods from being split by occasional level deviations. **Note:** Gap tolerance requires periods ≥90 minutes (6 intervals) to detect outliers effectively. Default: 0 (strict filtering, no tolerance).",
|
||||||
"enable_min_periods_best": "When enabled, filters will be gradually relaxed if not enough periods are found. This attempts to reach the desired minimum number of periods, which may include less optimal time windows as best-price periods.",
|
"enable_min_periods_best": "When enabled, filters will be gradually relaxed if not enough periods are found. This attempts to reach the desired minimum number of periods, which may include less optimal time windows as best-price periods.",
|
||||||
"min_periods_best": "Minimum number of best price periods to aim for per day. Filters will be relaxed step-by-step to try achieving this count. Only active when 'Try to Achieve Minimum Period Count' is enabled. Default: 1",
|
"min_periods_best": "Minimum number of best price periods to aim for per day. Filters will be relaxed step-by-step to try achieving this count. Only active when 'Try to Achieve Minimum Period Count' is enabled. Default: 1",
|
||||||
"relaxation_attempts_best": "How many flex levels (attempts) to try before giving up. Each attempt runs all filter combinations at the new flex level. More attempts increase the chance of finding additional periods at the cost of longer processing time."
|
"relaxation_attempts_best": "How many flex levels (attempts) to try before giving up. Each attempt runs all filter combinations at the new flex level. More attempts increase the chance of finding additional periods at the cost of longer processing time."
|
||||||
|
|
@ -150,7 +150,7 @@
|
||||||
"peak_price_flex": "Maximum below the daily maximum price that intervals can be and still qualify as 'peak price'. Recommended: -15 to -20 with relaxation enabled (default), or -25 to -35 without relaxation. Maximum: -50 (hard cap for reliable period detection). Note: Negative values indicate distance below maximum.",
|
"peak_price_flex": "Maximum below the daily maximum price that intervals can be and still qualify as 'peak price'. Recommended: -15 to -20 with relaxation enabled (default), or -25 to -35 without relaxation. Maximum: -50 (hard cap for reliable period detection). Note: Negative values indicate distance below maximum.",
|
||||||
"peak_price_min_distance_from_avg": "Ensures periods are significantly more expensive than the daily average, not just marginally above it. This filters out noise and prevents marking slightly-above-average periods as 'peak price' on days with flat prices. Higher values = stricter filtering (only truly expensive periods qualify). Default: 5 means periods must be at least 5% above the daily average.",
|
"peak_price_min_distance_from_avg": "Ensures periods are significantly more expensive than the daily average, not just marginally above it. This filters out noise and prevents marking slightly-above-average periods as 'peak price' on days with flat prices. Higher values = stricter filtering (only truly expensive periods qualify). Default: 5 means periods must be at least 5% above the daily average.",
|
||||||
"peak_price_min_level": "Only show peak price periods if they contain intervals with price levels ≥ selected value. For example, selecting 'Expensive' means the period must have at least one 'EXPENSIVE' or 'VERY_EXPENSIVE' interval. This ensures 'peak price' periods are not just relatively expensive for the day, but actually expensive in absolute terms. Select 'Any' to show peak prices regardless of their absolute price level.",
|
"peak_price_min_level": "Only show peak price periods if they contain intervals with price levels ≥ selected value. For example, selecting 'Expensive' means the period must have at least one 'EXPENSIVE' or 'VERY_EXPENSIVE' interval. This ensures 'peak price' periods are not just relatively expensive for the day, but actually expensive in absolute terms. Select 'Any' to show peak prices regardless of their absolute price level.",
|
||||||
"peak_price_max_level_gap_count": "Maximum number of consecutive intervals allowed that deviate by exactly one level step from the required level. For example: with 'Expensive' filter and gap count 2, a sequence 'EXPENSIVE, NORMAL, NORMAL, EXPENSIVE' is accepted (NORMAL is one step below EXPENSIVE). This prevents periods from being split by occasional level deviations. Default: 0 (strict filtering, no tolerance).",
|
"peak_price_max_level_gap_count": "Maximum number of consecutive intervals allowed that deviate by exactly one level step from the required level. For example: with 'Expensive' filter and gap count 2, a sequence 'EXPENSIVE, NORMAL, NORMAL, EXPENSIVE' is accepted (NORMAL is one step below EXPENSIVE). This prevents periods from being split by occasional level deviations. **Note:** Gap tolerance requires periods ≥90 minutes (6 intervals) to detect outliers effectively. Default: 0 (strict filtering, no tolerance).",
|
||||||
"enable_min_periods_peak": "When enabled, filters will be gradually relaxed if not enough periods are found. This attempts to reach the desired minimum number of periods to ensure you're warned about expensive periods even on days with unusual price patterns.",
|
"enable_min_periods_peak": "When enabled, filters will be gradually relaxed if not enough periods are found. This attempts to reach the desired minimum number of periods to ensure you're warned about expensive periods even on days with unusual price patterns.",
|
||||||
"min_periods_peak": "Minimum number of peak price periods to aim for per day. Filters will be relaxed step-by-step to try achieving this count. Only active when 'Try to Achieve Minimum Period Count' is enabled. Default: 1",
|
"min_periods_peak": "Minimum number of peak price periods to aim for per day. Filters will be relaxed step-by-step to try achieving this count. Only active when 'Try to Achieve Minimum Period Count' is enabled. Default: 1",
|
||||||
"relaxation_attempts_peak": "How many flex levels (attempts) to try before giving up. Each attempt runs all filter combinations at the new flex level. More attempts increase the chance of finding additional peak periods at the cost of longer processing time."
|
"relaxation_attempts_peak": "How many flex levels (attempts) to try before giving up. Each attempt runs all filter combinations at the new flex level. More attempts increase the chance of finding additional peak periods at the cost of longer processing time."
|
||||||
|
|
@ -197,7 +197,19 @@
|
||||||
"unknown": "An unexpected error occurred. Please check the logs for details.",
|
"unknown": "An unexpected error occurred. Please check the logs for details.",
|
||||||
"cannot_connect": "Failed to connect",
|
"cannot_connect": "Failed to connect",
|
||||||
"invalid_access_token": "Invalid access token",
|
"invalid_access_token": "Invalid access token",
|
||||||
"different_home": "The access token is not valid for the home ID this integration is configured for."
|
"different_home": "The access token is not valid for the home ID this integration is configured for.",
|
||||||
|
"invalid_period_length": "Period length must be at least 15 minutes (multiples of 15).",
|
||||||
|
"invalid_flex": "Flexibility percentage must be between -50% and +50%",
|
||||||
|
"invalid_distance": "Distance percentage must be between 0% and 50%",
|
||||||
|
"invalid_min_periods": "Minimum periods count must be between 1 and 10",
|
||||||
|
"invalid_gap_count": "Gap count must be between 0 and 8",
|
||||||
|
"invalid_relaxation_attempts": "Relaxation attempts must be between 1 and 12",
|
||||||
|
"invalid_price_rating_low": "Low price rating threshold must be between -50% and -5%",
|
||||||
|
"invalid_price_rating_high": "High price rating threshold must be between 5% and 50%",
|
||||||
|
"invalid_price_rating_thresholds": "Low threshold must be less than high threshold",
|
||||||
|
"invalid_volatility_threshold": "Volatility threshold must be between 0% and 100%",
|
||||||
|
"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%"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"entry_not_found": "Tibber configuration entry not found."
|
"entry_not_found": "Tibber configuration entry not found."
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@
|
||||||
"enable_min_periods_best": "Når aktivert vil filtrene gradvis bli lempeligere hvis det ikke blir funnet nok perioder. Dette forsøker å nå ønsket minimum antall perioder, noe som kan føre til at mindre optimale tidsrom blir markert som beste-pris-perioder.",
|
"enable_min_periods_best": "Når aktivert vil filtrene gradvis bli lempeligere hvis det ikke blir funnet nok perioder. Dette forsøker å nå ønsket minimum antall perioder, noe som kan føre til at mindre optimale tidsrom blir markert som beste-pris-perioder.",
|
||||||
"min_periods_best": "Minimum antall beste-pris-perioder å sikte mot per dag. Filtre vil bli lempet trinn for trinn for å prøve å oppnå dette antallet. Kun aktiv når 'Prøv å oppnå minimum antall perioder' er aktivert. Standard: 1",
|
"min_periods_best": "Minimum antall beste-pris-perioder å sikte mot per dag. Filtre vil bli lempet trinn for trinn for å prøve å oppnå dette antallet. Kun aktiv når 'Prøv å oppnå minimum antall perioder' er aktivert. Standard: 1",
|
||||||
"relaxation_attempts_best": "Hvor mange fleksnivåer (forsøk) som testes før vi gir opp. Hvert forsøk kjører alle filterkombinasjoner på det nye fleksnivået. Flere forsøk øker sjansen for ekstra perioder, men tar litt lengre tid.",
|
"relaxation_attempts_best": "Hvor mange fleksnivåer (forsøk) som testes før vi gir opp. Hvert forsøk kjører alle filterkombinasjoner på det nye fleksnivået. Flere forsøk øker sjansen for ekstra perioder, men tar litt lengre tid.",
|
||||||
"best_price_max_level_gap_count": "Maksimalt antall påfølgende intervaller som kan avvike med nøyaktig étt nivåtrinn fra det nødvendige nivået. For eksempel: med 'Billig' filter og gapantall 1, aksepteres sekvensen 'BILLIG, BILLIG, NORMAL, BILLIG' (NORMAL er étt trinn over BILLIG). Dette forhindrer at perioder blir delt opp av tilfeldige nivåavvik. Standard: 0 (streng filtrering, ingen toleranse)."
|
"best_price_max_level_gap_count": "Maksimalt antall påfølgende intervaller som kan avvike med nøyaktig étt nivåtrinn fra det nødvendige nivået. For eksempel: med 'Billig' filter og gapantall 1, aksepteres sekvensen 'BILLIG, BILLIG, NORMAL, BILLIG' (NORMAL er étt trinn over BILLIG). Dette forhindrer at perioder blir delt opp av tilfeldige nivåavvik. **Merk:** Gaptoleranse krever perioder ≥90 minutter (6 intervaller) for å oppdage avvik effektivt. Standard: 0 (streng filtrering, ingen toleranse)."
|
||||||
},
|
},
|
||||||
"submit": "Fortsett →"
|
"submit": "Fortsett →"
|
||||||
},
|
},
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
"enable_min_periods_peak": "Når aktivert vil filtrene gradvis bli lempeligere hvis det ikke blir funnet nok perioder. Dette forsøker å nå ønsket minimum antall perioder for å sikre at du blir advart om dyre perioder selv på dager med uvanlige prismønstre.",
|
"enable_min_periods_peak": "Når aktivert vil filtrene gradvis bli lempeligere hvis det ikke blir funnet nok perioder. Dette forsøker å nå ønsket minimum antall perioder for å sikre at du blir advart om dyre perioder selv på dager med uvanlige prismønstre.",
|
||||||
"min_periods_peak": "Minimum antall topp-pris-perioder å sikte mot per dag. Filtre vil bli lempet trinn for trinn for å prøve å oppnå dette antallet. Kun aktiv når 'Prøv å oppnå minimum antall perioder' er aktivert. Standard: 1",
|
"min_periods_peak": "Minimum antall topp-pris-perioder å sikte mot per dag. Filtre vil bli lempet trinn for trinn for å prøve å oppnå dette antallet. Kun aktiv når 'Prøv å oppnå minimum antall perioder' er aktivert. Standard: 1",
|
||||||
"relaxation_attempts_peak": "Hvor mange fleksnivåer (forsøk) som testes før vi gir opp. Hvert forsøk kjører alle filterkombinasjoner på det nye fleksnivået. Flere forsøk øker sjansen for ekstra toppprisperioder, men tar litt lengre tid.",
|
"relaxation_attempts_peak": "Hvor mange fleksnivåer (forsøk) som testes før vi gir opp. Hvert forsøk kjører alle filterkombinasjoner på det nye fleksnivået. Flere forsøk øker sjansen for ekstra toppprisperioder, men tar litt lengre tid.",
|
||||||
"peak_price_max_level_gap_count": "Maksimalt antall påfølgende intervaller som kan avvike med nøyaktig étt nivåtrinn fra det nødvendige nivået. For eksempel: med 'Dyr' filter og gapantall 1, aksepteres sekvensen 'DYR, DYR, NORMAL, DYR' (NORMAL er étt trinn under DYR). Dette forhindrer at perioder blir delt opp av tilfeldige nivåavvik. Standard: 0 (streng filtrering, ingen toleranse)."
|
"peak_price_max_level_gap_count": "Maksimalt antall påfølgende intervaller som kan avvike med nøyaktig étt nivåtrinn fra det nødvendige nivået. For eksempel: med 'Dyr' filter og gapantall 1, aksepteres sekvensen 'DYR, DYR, NORMAL, DYR' (NORMAL er étt trinn under DYR). Dette forhindrer at perioder blir delt opp av tilfeldige nivåavvik. **Merk:** Gaptoleranse krever perioder ≥90 minutter (6 intervaller) for å oppdage avvik effektivt. Standard: 0 (streng filtrering, ingen toleranse)."
|
||||||
},
|
},
|
||||||
"submit": "Fortsett →"
|
"submit": "Fortsett →"
|
||||||
},
|
},
|
||||||
|
|
@ -197,7 +197,19 @@
|
||||||
"unknown": "En uventet feil oppstod. Vennligst sjekk loggene for detaljer.",
|
"unknown": "En uventet feil oppstod. Vennligst sjekk loggene for detaljer.",
|
||||||
"cannot_connect": "Kunne ikke koble til",
|
"cannot_connect": "Kunne ikke koble til",
|
||||||
"invalid_access_token": "Ugyldig tilgangstoken",
|
"invalid_access_token": "Ugyldig tilgangstoken",
|
||||||
"different_home": "Tilgangstokenet er ikke gyldig for hjem-ID-en denne integrasjonen er konfigurert for."
|
"different_home": "Tilgangstokenet er ikke gyldig for hjem-ID-en denne integrasjonen er konfigurert for.",
|
||||||
|
"invalid_flex": "TRANSLATE: Flexibility percentage must be between -50% and +50%",
|
||||||
|
"invalid_distance": "TRANSLATE: Distance percentage must be between 0% and 50%",
|
||||||
|
"invalid_min_periods": "TRANSLATE: Minimum periods count must be between 1 and 10",
|
||||||
|
"invalid_period_length": "Periodelengden må være minst 15 minutter (multipler av 15).",
|
||||||
|
"invalid_gap_count": "Gaptoleranse må være mellom 0 og 8",
|
||||||
|
"invalid_relaxation_attempts": "Lempingsforsøk må være mellom 1 og 12",
|
||||||
|
"invalid_price_rating_low": "Lav prisvurderingsgrense må være mellom -50% og -5%",
|
||||||
|
"invalid_price_rating_high": "Høy prisvurderingsgrense må være mellom 5% og 50%",
|
||||||
|
"invalid_price_rating_thresholds": "Lav grense må være mindre enn høy grense",
|
||||||
|
"invalid_volatility_threshold": "Volatilitetsgrense må være mellom 0% og 100%",
|
||||||
|
"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%"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet."
|
"entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet."
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@
|
||||||
"enable_min_periods_best": "Wanneer ingeschakeld worden filters geleidelijk versoepeld als er niet genoeg periodes worden gevonden. Dit probeert het gewenste minimum aantal periodes te bereiken om ervoor te zorgen dat je kansen hebt om van lage prijzen te profiteren, zelfs op dagen met ongebruikelijke prijspatronen.",
|
"enable_min_periods_best": "Wanneer ingeschakeld worden filters geleidelijk versoepeld als er niet genoeg periodes worden gevonden. Dit probeert het gewenste minimum aantal periodes te bereiken om ervoor te zorgen dat je kansen hebt om van lage prijzen te profiteren, zelfs op dagen met ongebruikelijke prijspatronen.",
|
||||||
"min_periods_best": "Minimum aantal beste prijsperiodes om naar te streven per dag. Filters worden stap voor stap versoepeld om dit aantal te proberen bereiken. Alleen actief wanneer 'Probeer minimum aantal periodes te bereiken' is ingeschakeld. Standaard: 1",
|
"min_periods_best": "Minimum aantal beste prijsperiodes om naar te streven per dag. Filters worden stap voor stap versoepeld om dit aantal te proberen bereiken. Alleen actief wanneer 'Probeer minimum aantal periodes te bereiken' is ingeschakeld. Standaard: 1",
|
||||||
"relaxation_attempts_best": "Hoeveel keer de ontspanningslogica filters opnieuw mag proberen. Gebruik hogere waarden om meer variaties te testen als dagen extreem grillig zijn. Hogere aantallen vergen meer rekentijd maar vergroten de kans dat het gewenste minimum aantal periodes wordt gehaald.",
|
"relaxation_attempts_best": "Hoeveel keer de ontspanningslogica filters opnieuw mag proberen. Gebruik hogere waarden om meer variaties te testen als dagen extreem grillig zijn. Hogere aantallen vergen meer rekentijd maar vergroten de kans dat het gewenste minimum aantal periodes wordt gehaald.",
|
||||||
"best_price_max_level_gap_count": "Maximum aantal opeenvolgende intervallen dat precies één niveaustap mag afwijken van het vereiste niveau. Bijvoorbeeld: met 'Goedkoop' filter en gaptelling 1 wordt de reeks 'GOEDKOOP, GOEDKOOP, NORMAAL, GOEDKOOP' geaccepteerd (NORMAAL is één stap boven GOEDKOOP). Dit voorkomt dat periodes worden opgesplitst door incidentele niveauafwijkingen. Standaard: 0 (strikte filtering, geen tolerantie)."
|
"best_price_max_level_gap_count": "Maximum aantal opeenvolgende intervallen dat precies één niveaustap mag afwijken van het vereiste niveau. Bijvoorbeeld: met 'Goedkoop' filter en gaptelling 1 wordt de reeks 'GOEDKOOP, GOEDKOOP, NORMAAL, GOEDKOOP' geaccepteerd (NORMAAL is één stap boven GOEDKOOP). Dit voorkomt dat periodes worden opgesplitst door incidentele niveauafwijkingen. **Let op:** Gaptolerantie vereist periodes ≥90 minuten (6 intervallen) om afwijkingen effectief te detecteren. Standaard: 0 (strikte filtering, geen tolerantie)."
|
||||||
},
|
},
|
||||||
"submit": "Doorgaan →"
|
"submit": "Doorgaan →"
|
||||||
},
|
},
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
"enable_min_periods_peak": "Wanneer ingeschakeld worden filters geleidelijk versoepeld als er niet genoeg periodes worden gevonden. Dit probeert het gewenste minimum aantal periodes te bereiken om ervoor te zorgen dat je wordt gewaarschuwd voor dure periodes, zelfs op dagen met ongebruikelijke prijspatronen.",
|
"enable_min_periods_peak": "Wanneer ingeschakeld worden filters geleidelijk versoepeld als er niet genoeg periodes worden gevonden. Dit probeert het gewenste minimum aantal periodes te bereiken om ervoor te zorgen dat je wordt gewaarschuwd voor dure periodes, zelfs op dagen met ongebruikelijke prijspatronen.",
|
||||||
"min_periods_peak": "Minimum aantal piekprijsperiodes om naar te streven per dag. Filters worden stap voor stap versoepeld om dit aantal te proberen bereiken. Alleen actief wanneer 'Probeer minimum aantal periodes te bereiken' is ingeschakeld. Standaard: 1",
|
"min_periods_peak": "Minimum aantal piekprijsperiodes om naar te streven per dag. Filters worden stap voor stap versoepeld om dit aantal te proberen bereiken. Alleen actief wanneer 'Probeer minimum aantal periodes te bereiken' is ingeschakeld. Standaard: 1",
|
||||||
"relaxation_attempts_peak": "Hoeveel keer de ontspanningslogica filters opnieuw mag proberen. Gebruik meer pogingen wanneer de piekperiodes moeilijk te vinden zijn door vlakke of zeer grillige dagen. Elke extra poging kost wat extra verwerkingstijd maar vergroot de kans dat periodes worden gevonden.",
|
"relaxation_attempts_peak": "Hoeveel keer de ontspanningslogica filters opnieuw mag proberen. Gebruik meer pogingen wanneer de piekperiodes moeilijk te vinden zijn door vlakke of zeer grillige dagen. Elke extra poging kost wat extra verwerkingstijd maar vergroot de kans dat periodes worden gevonden.",
|
||||||
"peak_price_max_level_gap_count": "Maximum aantal opeenvolgende intervallen dat precies één niveaustap mag afwijken van het vereiste niveau. Bijvoorbeeld: met 'Duur' filter en gaptelling 1 wordt de reeks 'DUUR, DUUR, NORMAAL, DUUR' geaccepteerd (NORMAAL is één stap onder DUUR). Dit voorkomt dat periodes worden opgesplitst door incidentele niveauafwijkingen. Standaard: 0 (strikte filtering, geen tolerantie)."
|
"peak_price_max_level_gap_count": "Maximum aantal opeenvolgende intervallen dat precies één niveaustap mag afwijken van het vereiste niveau. Bijvoorbeeld: met 'Duur' filter en gaptelling 1 wordt de reeks 'DUUR, DUUR, NORMAAL, DUUR' geaccepteerd (NORMAAL is één stap onder DUUR). Dit voorkomt dat periodes worden opgesplitst door incidentele niveauafwijkingen. **Let op:** Gaptolerantie vereist periodes ≥90 minuten (6 intervallen) om afwijkingen effectief te detecteren. Standaard: 0 (strikte filtering, geen tolerantie)."
|
||||||
},
|
},
|
||||||
"submit": "Doorgaan →"
|
"submit": "Doorgaan →"
|
||||||
},
|
},
|
||||||
|
|
@ -197,7 +197,19 @@
|
||||||
"unknown": "Er is een onverwachte fout opgetreden. Controleer de logboeken voor details.",
|
"unknown": "Er is een onverwachte fout opgetreden. Controleer de logboeken voor details.",
|
||||||
"cannot_connect": "Verbinding mislukt",
|
"cannot_connect": "Verbinding mislukt",
|
||||||
"invalid_access_token": "Ongeldig toegangstoken",
|
"invalid_access_token": "Ongeldig toegangstoken",
|
||||||
"different_home": "Het toegangstoken is niet geldig voor de huis-ID waarvoor deze integratie is geconfigureerd."
|
"different_home": "Het toegangstoken is niet geldig voor de huis-ID waarvoor deze integratie is geconfigureerd.",
|
||||||
|
"invalid_flex": "TRANSLATE: Flexibility percentage must be between -50% and +50%",
|
||||||
|
"invalid_distance": "TRANSLATE: Distance percentage must be between 0% and 50%",
|
||||||
|
"invalid_min_periods": "TRANSLATE: Minimum periods count must be between 1 and 10",
|
||||||
|
"invalid_period_length": "De periodelengte moet minimaal 15 minuten zijn (veelvouden van 15).",
|
||||||
|
"invalid_gap_count": "Gaptolerantie moet tussen 0 en 8 liggen",
|
||||||
|
"invalid_relaxation_attempts": "Versoepelingspogingen moeten tussen 1 en 12 liggen",
|
||||||
|
"invalid_price_rating_low": "Lage prijsbeoordelingsdrempel moet tussen -50% en -5% liggen",
|
||||||
|
"invalid_price_rating_high": "Hoge prijsbeoordelingsdrempel moet tussen 5% en 50% liggen",
|
||||||
|
"invalid_price_rating_thresholds": "Lage drempel moet lager zijn dan hoge drempel",
|
||||||
|
"invalid_volatility_threshold": "Volatiliteitsdrempel moet tussen 0% en 100% liggen",
|
||||||
|
"invalid_price_trend_rising": "Stijgende trenddrempel moet tussen 1% en 50% liggen",
|
||||||
|
"invalid_price_trend_falling": "Dalende trenddrempel moet tussen -50% en -1% liggen"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"entry_not_found": "Tibber-configuratie-item niet gevonden."
|
"entry_not_found": "Tibber-configuratie-item niet gevonden."
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@
|
||||||
"enable_min_periods_best": "När aktiverad kommer filtren att gradvis luckras upp om inte tillräckligt många perioder hittas. Detta försöker uppnå det önskade minsta antalet perioder för att säkerställa att du har möjligheter att dra nytta av låga priser även på dagar med ovanliga prismönster.",
|
"enable_min_periods_best": "När aktiverad kommer filtren att gradvis luckras upp om inte tillräckligt många perioder hittas. Detta försöker uppnå det önskade minsta antalet perioder för att säkerställa att du har möjligheter att dra nytta av låga priser även på dagar med ovanliga prismönster.",
|
||||||
"min_periods_best": "Minsta antal bästa prisperioder att sträva efter per dag. Filtren kommer att luckras upp steg för steg för att försöka uppnå detta antal. Endast aktiv när 'Försök uppnå minsta antal perioder' är aktiverad. Standard: 1",
|
"min_periods_best": "Minsta antal bästa prisperioder att sträva efter per dag. Filtren kommer att luckras upp steg för steg för att försöka uppnå detta antal. Endast aktiv när 'Försök uppnå minsta antal perioder' är aktiverad. Standard: 1",
|
||||||
"relaxation_attempts_best": "Hur många gånger avslappningslogiken får försöka hitta nya kombinationer av flex och filter. Öka detta om dagarna är extrema och du behöver fler försök för att nå minimikravet. Varje extra försök tar lite mer tid men ökar chansen att hitta perioder.",
|
"relaxation_attempts_best": "Hur många gånger avslappningslogiken får försöka hitta nya kombinationer av flex och filter. Öka detta om dagarna är extrema och du behöver fler försök för att nå minimikravet. Varje extra försök tar lite mer tid men ökar chansen att hitta perioder.",
|
||||||
"best_price_max_level_gap_count": "Maximalt antal på varandra följande intervaller som får avvika med exakt ett nivåsteg från det erforderliga nivået. Till exempel: med 'Billigt' filter och gapantal 1 accepteras sekvensen 'BILLIGT, BILLIGT, NORMALT, BILLIGT' (NORMALT är ett steg över BILLIGT). Detta förhindrar att perioder delas upp av tillfälliga nivåavvikelser. Standard: 0 (strikt filtrering, ingen tolerans)."
|
"best_price_max_level_gap_count": "Maximalt antal på varandra följande intervaller som får avvika med exakt ett nivåsteg från det erforderliga nivået. Till exempel: med 'Billigt' filter och gapantal 1 accepteras sekvensen 'BILLIGT, BILLIGT, NORMALT, BILLIGT' (NORMALT är ett steg över BILLIGT). Detta förhindrar att perioder delas upp av tillfälliga nivåavvikelser. **Obs:** Gaptoleransen kräver perioder ≥90 minuter (6 intervaller) för att detektera avvikelser effektivt. Standard: 0 (strikt filtrering, ingen tolerans)."
|
||||||
},
|
},
|
||||||
"submit": "Fortsätt →"
|
"submit": "Fortsätt →"
|
||||||
},
|
},
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
"enable_min_periods_peak": "När aktiverad kommer filtren att gradvis luckras upp om inte tillräckligt många perioder hittas. Detta försöker uppnå det önskade minsta antalet perioder för att säkerställa att du blir varnad för dyra perioder även på dagar med ovanliga prismönster.",
|
"enable_min_periods_peak": "När aktiverad kommer filtren att gradvis luckras upp om inte tillräckligt många perioder hittas. Detta försöker uppnå det önskade minsta antalet perioder för att säkerställa att du blir varnad för dyra perioder även på dagar med ovanliga prismönster.",
|
||||||
"min_periods_peak": "Minsta antal topprisperioder att sträva efter per dag. Filtren kommer att luckras upp steg för steg för att försöka uppnå detta antal. Endast aktiv när 'Försök uppnå minsta antal perioder' är aktiverad. Standard: 1",
|
"min_periods_peak": "Minsta antal topprisperioder att sträva efter per dag. Filtren kommer att luckras upp steg för steg för att försöka uppnå detta antal. Endast aktiv när 'Försök uppnå minsta antal perioder' är aktiverad. Standard: 1",
|
||||||
"relaxation_attempts_peak": "Hur många gånger avslappningslogiken får försöka hitta nya kombinationer av flex och filter. Öka detta när topperioderna är svåra att hitta på grund av platta eller mycket volatila dagar. Fler försök ger större chans att hitta perioder men kräver lite mer beräkningstid.",
|
"relaxation_attempts_peak": "Hur många gånger avslappningslogiken får försöka hitta nya kombinationer av flex och filter. Öka detta när topperioderna är svåra att hitta på grund av platta eller mycket volatila dagar. Fler försök ger större chans att hitta perioder men kräver lite mer beräkningstid.",
|
||||||
"peak_price_max_level_gap_count": "Maximalt antal på varandra följande intervaller som får avvika med exakt ett nivåsteg från det erforderliga nivået. Till exempel: med 'Dyrt' filter och gapantal 1 accepteras sekvensen 'DYRT, DYRT, NORMALT, DYRT' (NORMALT är ett steg under DYRT). Detta förhindrar att perioder delas upp av tillfälliga nivåavvikelser. Standard: 0 (strikt filtrering, ingen tolerans)."
|
"peak_price_max_level_gap_count": "Maximalt antal på varandra följande intervaller som får avvika med exakt ett nivåsteg från det erforderliga nivået. Till exempel: med 'Dyrt' filter och gapantal 1 accepteras sekvensen 'DYRT, DYRT, NORMALT, DYRT' (NORMALT är ett steg under DYRT). Detta förhindrar att perioder delas upp av tillfälliga nivåavvikelser. **Obs:** Gaptoleransen kräver perioder ≥90 minuter (6 intervaller) för att detektera avvikelser effektivt. Standard: 0 (strikt filtrering, ingen tolerans)."
|
||||||
},
|
},
|
||||||
"submit": "Fortsätt →"
|
"submit": "Fortsätt →"
|
||||||
},
|
},
|
||||||
|
|
@ -197,7 +197,19 @@
|
||||||
"unknown": "Ett oväntat fel inträffade. Vänligen kontrollera loggarna för detaljer.",
|
"unknown": "Ett oväntat fel inträffade. Vänligen kontrollera loggarna för detaljer.",
|
||||||
"cannot_connect": "Kunde inte ansluta",
|
"cannot_connect": "Kunde inte ansluta",
|
||||||
"invalid_access_token": "Ogiltig åtkomsttoken",
|
"invalid_access_token": "Ogiltig åtkomsttoken",
|
||||||
"different_home": "Åtkomsttoken är inte giltig för hem-ID:t som denna integration är konfigurerad för."
|
"different_home": "Åtkomsttoken är inte giltig för hem-ID:t som denna integration är konfigurerad för.",
|
||||||
|
"invalid_flex": "TRANSLATE: Flexibility percentage must be between -50% and +50%",
|
||||||
|
"invalid_distance": "TRANSLATE: Distance percentage must be between 0% and 50%",
|
||||||
|
"invalid_min_periods": "TRANSLATE: Minimum periods count must be between 1 and 10",
|
||||||
|
"invalid_period_length": "Periodlängden måste vara minst 15 minuter (multiplar av 15).",
|
||||||
|
"invalid_gap_count": "Gaptolerans måste vara mellan 0 och 8",
|
||||||
|
"invalid_relaxation_attempts": "Avslappningsförsök måste vara mellan 1 och 12",
|
||||||
|
"invalid_price_rating_low": "Låg prisklassificeringströskel måste vara mellan -50% och -5%",
|
||||||
|
"invalid_price_rating_high": "Hög prisklassificeringströskel måste vara mellan 5% och 50%",
|
||||||
|
"invalid_price_rating_thresholds": "Låg tröskel måste vara mindre än hög tröskel",
|
||||||
|
"invalid_volatility_threshold": "Volatilitetströskel måste vara mellan 0% och 100%",
|
||||||
|
"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%"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"entry_not_found": "Tibber-konfigurationspost hittades inte."
|
"entry_not_found": "Tibber-konfigurationspost hittades inte."
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue