mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-04-09 09:03:40 +00:00
feat(config_flow): add trend confirmation and noise floor settings
Added 3 new config fields to price trend options step: - Trend Change Confirmation (2-6 intervals slider) - Min Price Change for trend (display-unit-aware slider) - Min Price Change for strong trend (display-unit-aware slider) Price change sliders scale between base currency (EUR/NOK) storage and display unit (ct/øre) presentation using get_display_unit_factor(). Added migration in __init__.py to convert old display-unit values to base currency format. Impact: Users can tune trend sensitivity: higher confirmation = fewer false changes, higher min price change = no trends from tiny fluctuations.
This commit is contained in:
parent
91efeed90f
commit
798de5946d
3 changed files with 135 additions and 2 deletions
|
|
@ -21,11 +21,15 @@ from homeassistant.loader import async_get_loaded_integration
|
||||||
from .api import TibberPricesApiClient
|
from .api import TibberPricesApiClient
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CURRENCY_DISPLAY_MODE,
|
CONF_CURRENCY_DISPLAY_MODE,
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
DATA_CHART_CONFIG,
|
DATA_CHART_CONFIG,
|
||||||
DATA_CHART_METADATA_CONFIG,
|
DATA_CHART_METADATA_CONFIG,
|
||||||
DISPLAY_MODE_SUBUNIT,
|
DISPLAY_MODE_SUBUNIT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
|
MAX_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
async_load_standard_translations,
|
async_load_standard_translations,
|
||||||
async_load_translations,
|
async_load_translations,
|
||||||
)
|
)
|
||||||
|
|
@ -141,6 +145,29 @@ async def _migrate_config_options(hass: HomeAssistant, entry: ConfigEntry) -> No
|
||||||
DISPLAY_MODE_SUBUNIT,
|
DISPLAY_MODE_SUBUNIT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Migration: Convert min_price_change from display currency (ct/øre) to base currency (EUR/NOK)
|
||||||
|
# Before this change, values were stored in display units. Now always stored in base currency.
|
||||||
|
# Detection: If either value exceeds its new max, both are in old format and need conversion.
|
||||||
|
# Old range: 0-5.0 ct / 0-10.0 ct, New range: 0-0.05 EUR / 0-0.10 EUR
|
||||||
|
normal_val = migrated.get(CONF_PRICE_TREND_MIN_PRICE_CHANGE)
|
||||||
|
strongly_val = migrated.get(CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY)
|
||||||
|
old_format_detected = (normal_val is not None and normal_val > MAX_PRICE_TREND_MIN_PRICE_CHANGE) or (
|
||||||
|
strongly_val is not None and strongly_val > MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY
|
||||||
|
)
|
||||||
|
if old_format_detected:
|
||||||
|
for key in (CONF_PRICE_TREND_MIN_PRICE_CHANGE, CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY):
|
||||||
|
if key in migrated and migrated[key] > 0:
|
||||||
|
old_val = migrated[key]
|
||||||
|
migrated[key] = round(old_val / 100, 6)
|
||||||
|
migration_performed = True
|
||||||
|
LOGGER.info(
|
||||||
|
"[%s] Migrated config: %s = %s -> %s (converted to base currency)",
|
||||||
|
entry.title,
|
||||||
|
key,
|
||||||
|
old_val,
|
||||||
|
migrated[key],
|
||||||
|
)
|
||||||
|
|
||||||
# Save migrated options if any changes were made
|
# Save migrated options if any changes were made
|
||||||
if migration_performed:
|
if migration_performed:
|
||||||
hass.config_entries.async_update_entry(entry, options=migrated)
|
hass.config_entries.async_update_entry(entry, options=migrated)
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ from custom_components.tibber_prices.const import (
|
||||||
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
||||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||||
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||||
|
|
@ -74,7 +76,10 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
async_get_translation,
|
async_get_translation,
|
||||||
|
format_price_unit_base,
|
||||||
|
format_price_unit_subunit,
|
||||||
get_default_options,
|
get_default_options,
|
||||||
|
get_display_unit_factor,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
|
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
@ -730,6 +735,9 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
"""Configure price trend thresholds."""
|
"""Configure price trend thresholds."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
# Get display factor for currency conversion
|
||||||
|
display_factor = get_display_unit_factor(self.config_entry)
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
# Schema is now flattened - fields come directly in user_input
|
# Schema is now flattened - fields come directly in user_input
|
||||||
# Store them flat in options (no nested structure)
|
# Store them flat in options (no nested structure)
|
||||||
|
|
@ -775,6 +783,15 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
)
|
)
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
|
# Convert min_price_change values from display unit to base currency for storage
|
||||||
|
# (dividing by 1 is a no-op for base currency mode)
|
||||||
|
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE] = round(
|
||||||
|
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE] / display_factor, 6
|
||||||
|
)
|
||||||
|
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY] = round(
|
||||||
|
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY] / display_factor, 6
|
||||||
|
)
|
||||||
|
|
||||||
# Store flat data directly in options (no section wrapping)
|
# Store flat data directly in options (no section wrapping)
|
||||||
self._options.update(user_input)
|
self._options.update(user_input)
|
||||||
# async_create_entry automatically handles change detection and listener triggering
|
# async_create_entry automatically handles change detection and listener triggering
|
||||||
|
|
@ -782,9 +799,20 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
||||||
# Return to menu for more changes
|
# Return to menu for more changes
|
||||||
return await self.async_step_init()
|
return await self.async_step_init()
|
||||||
|
|
||||||
|
# Get currency code for unit label on sliders
|
||||||
|
currency_code = self.config_entry.data.get("currency", None)
|
||||||
|
if display_factor > 1:
|
||||||
|
price_unit = format_price_unit_subunit(currency_code)
|
||||||
|
else:
|
||||||
|
price_unit = format_price_unit_base(currency_code)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
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,
|
||||||
|
display_factor=display_factor,
|
||||||
|
price_unit=price_unit,
|
||||||
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders=self._get_entity_warning_placeholders("price_trend"),
|
description_placeholders=self._get_entity_warning_placeholders("price_trend"),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ from custom_components.tibber_prices.const import (
|
||||||
CONF_PRICE_RATING_HYSTERESIS,
|
CONF_PRICE_RATING_HYSTERESIS,
|
||||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||||
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||||
|
|
@ -66,6 +69,9 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_PRICE_RATING_HYSTERESIS,
|
DEFAULT_PRICE_RATING_HYSTERESIS,
|
||||||
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
DEFAULT_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
|
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||||
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||||
|
|
@ -88,7 +94,10 @@ from custom_components.tibber_prices.const import (
|
||||||
MAX_PRICE_RATING_HYSTERESIS,
|
MAX_PRICE_RATING_HYSTERESIS,
|
||||||
MAX_PRICE_RATING_THRESHOLD_HIGH,
|
MAX_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
MAX_PRICE_RATING_THRESHOLD_LOW,
|
MAX_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
MAX_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
MAX_PRICE_TREND_FALLING,
|
MAX_PRICE_TREND_FALLING,
|
||||||
|
MAX_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
MAX_PRICE_TREND_RISING,
|
MAX_PRICE_TREND_RISING,
|
||||||
MAX_PRICE_TREND_STRONGLY_FALLING,
|
MAX_PRICE_TREND_STRONGLY_FALLING,
|
||||||
MAX_PRICE_TREND_STRONGLY_RISING,
|
MAX_PRICE_TREND_STRONGLY_RISING,
|
||||||
|
|
@ -103,7 +112,10 @@ from custom_components.tibber_prices.const import (
|
||||||
MIN_PRICE_RATING_HYSTERESIS,
|
MIN_PRICE_RATING_HYSTERESIS,
|
||||||
MIN_PRICE_RATING_THRESHOLD_HIGH,
|
MIN_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
MIN_PRICE_RATING_THRESHOLD_LOW,
|
MIN_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
MIN_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
MIN_PRICE_TREND_FALLING,
|
MIN_PRICE_TREND_FALLING,
|
||||||
|
MIN_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
MIN_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
MIN_PRICE_TREND_RISING,
|
MIN_PRICE_TREND_RISING,
|
||||||
MIN_PRICE_TREND_STRONGLY_FALLING,
|
MIN_PRICE_TREND_STRONGLY_FALLING,
|
||||||
MIN_PRICE_TREND_STRONGLY_RISING,
|
MIN_PRICE_TREND_STRONGLY_RISING,
|
||||||
|
|
@ -907,8 +919,16 @@ def get_peak_price_schema(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
def get_price_trend_schema(
|
||||||
|
options: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
display_factor: int = 1,
|
||||||
|
price_unit: str = "",
|
||||||
|
) -> vol.Schema:
|
||||||
"""Return schema for price trend thresholds configuration."""
|
"""Return schema for price trend thresholds configuration."""
|
||||||
|
# Scale min_price_change values for display (stored in base currency, shown in display unit)
|
||||||
|
step = 0.1 if display_factor > 1 else 0.001
|
||||||
|
|
||||||
return vol.Schema(
|
return vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
|
|
@ -979,6 +999,64 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
|
default=int(
|
||||||
|
options.get(
|
||||||
|
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
|
DEFAULT_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
): NumberSelector(
|
||||||
|
NumberSelectorConfig(
|
||||||
|
min=MIN_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
|
max=MAX_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||||
|
step=1,
|
||||||
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
default=round(
|
||||||
|
float(
|
||||||
|
options.get(
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
* display_factor,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
): NumberSelector(
|
||||||
|
NumberSelectorConfig(
|
||||||
|
min=MIN_PRICE_TREND_MIN_PRICE_CHANGE * display_factor,
|
||||||
|
max=MAX_PRICE_TREND_MIN_PRICE_CHANGE * display_factor,
|
||||||
|
step=step,
|
||||||
|
unit_of_measurement=price_unit,
|
||||||
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
|
default=round(
|
||||||
|
float(
|
||||||
|
options.get(
|
||||||
|
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
|
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
* display_factor,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
): NumberSelector(
|
||||||
|
NumberSelectorConfig(
|
||||||
|
min=MIN_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY * display_factor,
|
||||||
|
max=MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY * display_factor,
|
||||||
|
step=step,
|
||||||
|
unit_of_measurement=price_unit,
|
||||||
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue