mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
fix: support main and subunit currency
This commit is contained in:
parent
be34e87fa6
commit
1c19cebff5
12 changed files with 348 additions and 178 deletions
|
|
@ -4,9 +4,15 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from custom_components.tibber_prices.const import get_display_unit_factor
|
||||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
||||||
|
|
||||||
|
# Constants for price display conversion
|
||||||
|
_SUBUNIT_FACTOR = 100 # Conversion factor for subunit currency (ct/øre)
|
||||||
|
_SUBUNIT_PRECISION = 2 # Decimal places for subunit currency
|
||||||
|
_BASE_PRECISION = 4 # Decimal places for base currency
|
||||||
|
|
||||||
# Import TypedDict definitions for documentation (not used in signatures)
|
# Import TypedDict definitions for documentation (not used in signatures)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -66,6 +72,7 @@ def get_price_intervals_attributes(
|
||||||
*,
|
*,
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
reverse_sort: bool,
|
reverse_sort: bool,
|
||||||
|
config_entry: TibberPricesConfigEntry,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""
|
"""
|
||||||
Build attributes for period-based sensors (best/peak price).
|
Build attributes for period-based sensors (best/peak price).
|
||||||
|
|
@ -76,11 +83,13 @@ def get_price_intervals_attributes(
|
||||||
1. Get period summaries from coordinator (already filtered and fully calculated)
|
1. Get period summaries from coordinator (already filtered and fully calculated)
|
||||||
2. Add the current timestamp
|
2. Add the current timestamp
|
||||||
3. Find current or next period based on time
|
3. Find current or next period based on time
|
||||||
|
4. Convert prices to display units based on user configuration
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coordinator_data: Coordinator data dict
|
coordinator_data: Coordinator data dict
|
||||||
time: TibberPricesTimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
reverse_sort: True for peak_price (highest first), False for best_price (lowest first)
|
reverse_sort: True for peak_price (highest first), False for best_price (lowest first)
|
||||||
|
config_entry: Config entry for display unit configuration
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Attributes dict with current/next period and all periods list
|
Attributes dict with current/next period and all periods list
|
||||||
|
|
@ -101,11 +110,20 @@ def get_price_intervals_attributes(
|
||||||
if not period_summaries:
|
if not period_summaries:
|
||||||
return build_no_periods_result(time=time)
|
return build_no_periods_result(time=time)
|
||||||
|
|
||||||
|
# Filter periods for today+tomorrow (sensors don't show yesterday's periods)
|
||||||
|
# Coordinator cache contains yesterday/today/tomorrow, but sensors only need today+tomorrow
|
||||||
|
now = time.now()
|
||||||
|
today_start = time.start_of_local_day(now)
|
||||||
|
filtered_periods = [period for period in period_summaries if period.get("end") and period["end"] >= today_start]
|
||||||
|
|
||||||
|
if not filtered_periods:
|
||||||
|
return build_no_periods_result(time=time)
|
||||||
|
|
||||||
# Find current or next period based on current time
|
# Find current or next period based on current time
|
||||||
current_period = None
|
current_period = None
|
||||||
|
|
||||||
# First pass: find currently active period
|
# First pass: find currently active period
|
||||||
for period in period_summaries:
|
for period in filtered_periods:
|
||||||
start = period.get("start")
|
start = period.get("start")
|
||||||
end = period.get("end")
|
end = period.get("end")
|
||||||
if start and end and time.is_current_interval(start, end):
|
if start and end and time.is_current_interval(start, end):
|
||||||
|
|
@ -114,14 +132,14 @@ def get_price_intervals_attributes(
|
||||||
|
|
||||||
# Second pass: find next future period if none is active
|
# Second pass: find next future period if none is active
|
||||||
if not current_period:
|
if not current_period:
|
||||||
for period in period_summaries:
|
for period in filtered_periods:
|
||||||
start = period.get("start")
|
start = period.get("start")
|
||||||
if start and time.is_in_future(start):
|
if start and time.is_in_future(start):
|
||||||
current_period = period
|
current_period = period
|
||||||
break
|
break
|
||||||
|
|
||||||
# Build final attributes
|
# Build final attributes (use filtered_periods for display)
|
||||||
return build_final_attributes_simple(current_period, period_summaries, time=time)
|
return build_final_attributes_simple(current_period, filtered_periods, time=time, config_entry=config_entry)
|
||||||
|
|
||||||
|
|
||||||
def build_no_periods_result(*, time: TibberPricesTimeService) -> dict:
|
def build_no_periods_result(*, time: TibberPricesTimeService) -> dict:
|
||||||
|
|
@ -166,28 +184,58 @@ def add_decision_attributes(attributes: dict, current_period: dict) -> None:
|
||||||
attributes["rating_difference_%"] = current_period["rating_difference_%"]
|
attributes["rating_difference_%"] = current_period["rating_difference_%"]
|
||||||
|
|
||||||
|
|
||||||
def add_price_attributes(attributes: dict, current_period: dict) -> None:
|
def add_price_attributes(attributes: dict, current_period: dict, factor: int) -> None:
|
||||||
"""Add price statistics attributes (priority 3)."""
|
"""
|
||||||
|
Add price statistics attributes (priority 3).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attributes: Target dict to add attributes to
|
||||||
|
current_period: Period dict with price data (in base currency)
|
||||||
|
factor: Display unit conversion factor (100 for subunit, 1 for base)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Convert prices from base currency to display units
|
||||||
|
precision = _SUBUNIT_PRECISION if factor == _SUBUNIT_FACTOR else _BASE_PRECISION
|
||||||
|
|
||||||
if "price_mean" in current_period:
|
if "price_mean" in current_period:
|
||||||
attributes["price_mean"] = current_period["price_mean"]
|
attributes["price_mean"] = round(current_period["price_mean"] * factor, precision)
|
||||||
if "price_median" in current_period:
|
if "price_median" in current_period:
|
||||||
attributes["price_median"] = current_period["price_median"]
|
attributes["price_median"] = round(current_period["price_median"] * factor, precision)
|
||||||
if "price_min" in current_period:
|
if "price_min" in current_period:
|
||||||
attributes["price_min"] = current_period["price_min"]
|
attributes["price_min"] = round(current_period["price_min"] * factor, precision)
|
||||||
if "price_max" in current_period:
|
if "price_max" in current_period:
|
||||||
attributes["price_max"] = current_period["price_max"]
|
attributes["price_max"] = round(current_period["price_max"] * factor, precision)
|
||||||
if "price_spread" in current_period:
|
if "price_spread" in current_period:
|
||||||
attributes["price_spread"] = current_period["price_spread"]
|
attributes["price_spread"] = round(current_period["price_spread"] * factor, precision)
|
||||||
if "volatility" in current_period:
|
if "volatility" in current_period:
|
||||||
attributes["volatility"] = current_period["volatility"]
|
attributes["volatility"] = current_period["volatility"] # Volatility is not a price, keep as-is
|
||||||
|
|
||||||
|
|
||||||
def add_comparison_attributes(attributes: dict, current_period: dict) -> None:
|
def add_comparison_attributes(attributes: dict, current_period: dict, factor: int) -> None:
|
||||||
"""Add price comparison attributes (priority 4)."""
|
"""
|
||||||
|
Add price comparison attributes (priority 4).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attributes: Target dict to add attributes to
|
||||||
|
current_period: Period dict with price diff data (in base currency)
|
||||||
|
factor: Display unit conversion factor (100 for subunit, 1 for base)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Convert price differences from base currency to display units
|
||||||
|
precision = _SUBUNIT_PRECISION if factor == _SUBUNIT_FACTOR else _BASE_PRECISION
|
||||||
|
|
||||||
if "period_price_diff_from_daily_min" in current_period:
|
if "period_price_diff_from_daily_min" in current_period:
|
||||||
attributes["period_price_diff_from_daily_min"] = current_period["period_price_diff_from_daily_min"]
|
attributes["period_price_diff_from_daily_min"] = round(
|
||||||
|
current_period["period_price_diff_from_daily_min"] * factor, precision
|
||||||
|
)
|
||||||
if "period_price_diff_from_daily_min_%" in current_period:
|
if "period_price_diff_from_daily_min_%" in current_period:
|
||||||
attributes["period_price_diff_from_daily_min_%"] = current_period["period_price_diff_from_daily_min_%"]
|
attributes["period_price_diff_from_daily_min_%"] = current_period["period_price_diff_from_daily_min_%"]
|
||||||
|
if "period_price_diff_from_daily_max" in current_period:
|
||||||
|
attributes["period_price_diff_from_daily_max"] = round(
|
||||||
|
current_period["period_price_diff_from_daily_max"] * factor, precision
|
||||||
|
)
|
||||||
|
if "period_price_diff_from_daily_max_%" in current_period:
|
||||||
|
attributes["period_price_diff_from_daily_max_%"] = current_period["period_price_diff_from_daily_max_%"]
|
||||||
|
|
||||||
|
|
||||||
def add_detail_attributes(attributes: dict, current_period: dict) -> None:
|
def add_detail_attributes(attributes: dict, current_period: dict) -> None:
|
||||||
|
|
@ -219,11 +267,51 @@ def add_relaxation_attributes(attributes: dict, current_period: dict) -> None:
|
||||||
attributes["relaxation_threshold_applied_%"] = current_period["relaxation_threshold_applied_%"]
|
attributes["relaxation_threshold_applied_%"] = current_period["relaxation_threshold_applied_%"]
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_periods_to_display_units(period_summaries: list[dict], factor: int) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Convert price values in periods array to display units.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
period_summaries: List of period dicts with price data (in base currency)
|
||||||
|
factor: Display unit conversion factor (100 for subunit, 1 for base)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
New list with converted period dicts
|
||||||
|
|
||||||
|
"""
|
||||||
|
precision = _SUBUNIT_PRECISION if factor == _SUBUNIT_FACTOR else _BASE_PRECISION
|
||||||
|
converted_periods = []
|
||||||
|
|
||||||
|
for period in period_summaries:
|
||||||
|
converted = period.copy()
|
||||||
|
|
||||||
|
# Convert all price fields
|
||||||
|
price_fields = ["price_mean", "price_median", "price_min", "price_max", "price_spread"]
|
||||||
|
for field in price_fields:
|
||||||
|
if field in converted:
|
||||||
|
converted[field] = round(converted[field] * factor, precision)
|
||||||
|
|
||||||
|
# Convert price differences (not percentages)
|
||||||
|
if "period_price_diff_from_daily_min" in converted:
|
||||||
|
converted["period_price_diff_from_daily_min"] = round(
|
||||||
|
converted["period_price_diff_from_daily_min"] * factor, precision
|
||||||
|
)
|
||||||
|
if "period_price_diff_from_daily_max" in converted:
|
||||||
|
converted["period_price_diff_from_daily_max"] = round(
|
||||||
|
converted["period_price_diff_from_daily_max"] * factor, precision
|
||||||
|
)
|
||||||
|
|
||||||
|
converted_periods.append(converted)
|
||||||
|
|
||||||
|
return converted_periods
|
||||||
|
|
||||||
|
|
||||||
def build_final_attributes_simple(
|
def build_final_attributes_simple(
|
||||||
current_period: dict | None,
|
current_period: dict | None,
|
||||||
period_summaries: list[dict],
|
period_summaries: list[dict],
|
||||||
*,
|
*,
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
|
config_entry: TibberPricesConfigEntry,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Build the final attributes dictionary from coordinator's period summaries.
|
Build the final attributes dictionary from coordinator's period summaries.
|
||||||
|
|
@ -232,6 +320,7 @@ def build_final_attributes_simple(
|
||||||
1. Adds the current timestamp (only thing calculated every 15min)
|
1. Adds the current timestamp (only thing calculated every 15min)
|
||||||
2. Uses the current/next period from summaries
|
2. Uses the current/next period from summaries
|
||||||
3. Adds nested period summaries
|
3. Adds nested period summaries
|
||||||
|
4. Converts prices to display units based on user configuration
|
||||||
|
|
||||||
Attributes are ordered following the documented priority:
|
Attributes are ordered following the documented priority:
|
||||||
1. Time information (timestamp, start, end, duration)
|
1. Time information (timestamp, start, end, duration)
|
||||||
|
|
@ -247,6 +336,7 @@ def build_final_attributes_simple(
|
||||||
current_period: The current or next period (already complete from coordinator)
|
current_period: The current or next period (already complete from coordinator)
|
||||||
period_summaries: All period summaries from coordinator
|
period_summaries: All period summaries from coordinator
|
||||||
time: TibberPricesTimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
|
config_entry: Config entry for display unit configuration
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Complete attributes dict with all fields
|
Complete attributes dict with all fields
|
||||||
|
|
@ -256,6 +346,9 @@ def build_final_attributes_simple(
|
||||||
current_minute = (now.minute // 15) * 15
|
current_minute = (now.minute // 15) * 15
|
||||||
timestamp = now.replace(minute=current_minute, second=0, microsecond=0)
|
timestamp = now.replace(minute=current_minute, second=0, microsecond=0)
|
||||||
|
|
||||||
|
# Get display unit factor (100 for subunit, 1 for base currency)
|
||||||
|
factor = get_display_unit_factor(config_entry)
|
||||||
|
|
||||||
if current_period:
|
if current_period:
|
||||||
# Build attributes in priority order using helper methods
|
# Build attributes in priority order using helper methods
|
||||||
attributes = {}
|
attributes = {}
|
||||||
|
|
@ -266,11 +359,11 @@ def build_final_attributes_simple(
|
||||||
# 2. Core decision attributes
|
# 2. Core decision attributes
|
||||||
add_decision_attributes(attributes, current_period)
|
add_decision_attributes(attributes, current_period)
|
||||||
|
|
||||||
# 3. Price statistics
|
# 3. Price statistics (converted to display units)
|
||||||
add_price_attributes(attributes, current_period)
|
add_price_attributes(attributes, current_period, factor)
|
||||||
|
|
||||||
# 4. Price differences
|
# 4. Price differences (converted to display units)
|
||||||
add_comparison_attributes(attributes, current_period)
|
add_comparison_attributes(attributes, current_period, factor)
|
||||||
|
|
||||||
# 5. Detail information
|
# 5. Detail information
|
||||||
add_detail_attributes(attributes, current_period)
|
add_detail_attributes(attributes, current_period)
|
||||||
|
|
@ -278,15 +371,15 @@ def build_final_attributes_simple(
|
||||||
# 6. Relaxation information (only if period was relaxed)
|
# 6. Relaxation information (only if period was relaxed)
|
||||||
add_relaxation_attributes(attributes, current_period)
|
add_relaxation_attributes(attributes, current_period)
|
||||||
|
|
||||||
# 7. Meta information (periods array)
|
# 7. Meta information (periods array - prices converted to display units)
|
||||||
attributes["periods"] = period_summaries
|
attributes["periods"] = _convert_periods_to_display_units(period_summaries, factor)
|
||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
# No current/next period found - return all periods with timestamp
|
# No current/next period found - return all periods with timestamp (prices converted)
|
||||||
return {
|
return {
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
"periods": period_summaries,
|
"periods": _convert_periods_to_display_units(period_summaries, factor),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,12 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
|
||||||
"""Return True if the current time is within a best price period."""
|
"""Return True if the current time is within a best price period."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
attrs = get_price_intervals_attributes(self.coordinator.data, reverse_sort=False, time=self.coordinator.time)
|
attrs = get_price_intervals_attributes(
|
||||||
|
self.coordinator.data,
|
||||||
|
reverse_sort=False,
|
||||||
|
time=self.coordinator.time,
|
||||||
|
config_entry=self.coordinator.config_entry,
|
||||||
|
)
|
||||||
if not attrs:
|
if not attrs:
|
||||||
return False # Should not happen, but safety fallback
|
return False # Should not happen, but safety fallback
|
||||||
start = attrs.get("start")
|
start = attrs.get("start")
|
||||||
|
|
@ -152,7 +157,12 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
|
||||||
"""Return True if the current time is within a peak price period."""
|
"""Return True if the current time is within a peak price period."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
attrs = get_price_intervals_attributes(self.coordinator.data, reverse_sort=True, time=self.coordinator.time)
|
attrs = get_price_intervals_attributes(
|
||||||
|
self.coordinator.data,
|
||||||
|
reverse_sort=True,
|
||||||
|
time=self.coordinator.time,
|
||||||
|
config_entry=self.coordinator.config_entry,
|
||||||
|
)
|
||||||
if not attrs:
|
if not attrs:
|
||||||
return False # Should not happen, but safety fallback
|
return False # Should not happen, but safety fallback
|
||||||
start = attrs.get("start")
|
start = attrs.get("start")
|
||||||
|
|
@ -270,9 +280,19 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
|
||||||
key = self.entity_description.key
|
key = self.entity_description.key
|
||||||
|
|
||||||
if key == "peak_price_period":
|
if key == "peak_price_period":
|
||||||
return get_price_intervals_attributes(self.coordinator.data, reverse_sort=True, time=self.coordinator.time)
|
return get_price_intervals_attributes(
|
||||||
|
self.coordinator.data,
|
||||||
|
reverse_sort=True,
|
||||||
|
time=self.coordinator.time,
|
||||||
|
config_entry=self.coordinator.config_entry,
|
||||||
|
)
|
||||||
if key == "best_price_period":
|
if key == "best_price_period":
|
||||||
return get_price_intervals_attributes(self.coordinator.data, reverse_sort=False, time=self.coordinator.time)
|
return get_price_intervals_attributes(
|
||||||
|
self.coordinator.data,
|
||||||
|
reverse_sort=False,
|
||||||
|
time=self.coordinator.time,
|
||||||
|
config_entry=self.coordinator.config_entry,
|
||||||
|
)
|
||||||
if key == "tomorrow_data_available":
|
if key == "tomorrow_data_available":
|
||||||
return self._get_tomorrow_data_available_attributes()
|
return self._get_tomorrow_data_available_attributes()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,16 @@ CURRENCY_INFO = {
|
||||||
"GBP": ("£", "p", "Pence"),
|
"GBP": ("£", "p", "Pence"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Base currency names: ISO code -> full currency name (in local language)
|
||||||
|
CURRENCY_NAMES = {
|
||||||
|
"EUR": "Euro",
|
||||||
|
"NOK": "Norske kroner",
|
||||||
|
"SEK": "Svenska kronor",
|
||||||
|
"DKK": "Danske kroner",
|
||||||
|
"USD": "US Dollar",
|
||||||
|
"GBP": "British Pound",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_currency_info(currency_code: str | None) -> tuple[str, str, str]:
|
def get_currency_info(currency_code: str | None) -> tuple[str, str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -228,6 +238,24 @@ def format_price_unit_subunit(currency_code: str | None) -> str:
|
||||||
return f"{subunit_symbol}/{UnitOfPower.KILO_WATT}{UnitOfTime.HOURS}"
|
return f"{subunit_symbol}/{UnitOfPower.KILO_WATT}{UnitOfTime.HOURS}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_currency_name(currency_code: str | None) -> str:
|
||||||
|
"""
|
||||||
|
Get the full name of the base currency.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
currency_code: ISO 4217 currency code (e.g., 'EUR', 'NOK', 'SEK')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Full currency name like 'Euro' or 'Norwegian Krone'
|
||||||
|
Defaults to 'Euro' if currency is not recognized
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not currency_code:
|
||||||
|
currency_code = "EUR"
|
||||||
|
|
||||||
|
return CURRENCY_NAMES.get(currency_code.upper(), CURRENCY_NAMES["EUR"])
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Currency Display Mode Configuration
|
# Currency Display Mode Configuration
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ def calculate_periods(
|
||||||
*,
|
*,
|
||||||
config: TibberPricesPeriodConfig,
|
config: TibberPricesPeriodConfig,
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
config_entry: Any, # ConfigEntry type
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Calculate price periods (best or peak) from price data.
|
Calculate price periods (best or peak) from price data.
|
||||||
|
|
@ -57,7 +56,6 @@ def calculate_periods(
|
||||||
config: Period configuration containing reverse_sort, flex, min_distance_from_avg,
|
config: Period configuration containing reverse_sort, flex, min_distance_from_avg,
|
||||||
min_period_length, threshold_low, and threshold_high.
|
min_period_length, threshold_low, and threshold_high.
|
||||||
time: TibberPricesTimeService instance (required).
|
time: TibberPricesTimeService instance (required).
|
||||||
config_entry: Config entry to get display unit configuration.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with:
|
Dict with:
|
||||||
|
|
@ -185,12 +183,14 @@ def calculate_periods(
|
||||||
# Step 5: Add interval ends
|
# Step 5: Add interval ends
|
||||||
add_interval_ends(raw_periods, time=time)
|
add_interval_ends(raw_periods, time=time)
|
||||||
|
|
||||||
# Step 6: Filter periods by end date (keep periods ending today or later)
|
# Step 6: Filter periods by end date (keep periods ending yesterday or later)
|
||||||
|
# This ensures coordinator cache contains yesterday/today/tomorrow periods
|
||||||
|
# Sensors filter further for today+tomorrow, services can access all cached periods
|
||||||
raw_periods = filter_periods_by_end_date(raw_periods, time=time)
|
raw_periods = filter_periods_by_end_date(raw_periods, time=time)
|
||||||
|
|
||||||
# Step 8: Extract lightweight period summaries (no full price data)
|
# Step 8: Extract lightweight period summaries (no full price data)
|
||||||
# Note: Filtering for current/future is done here based on end date,
|
# Note: Periods are filtered by end date to keep yesterday/today/tomorrow.
|
||||||
# not start date. This preserves periods that started yesterday but end today.
|
# This preserves periods that started day-before-yesterday but end yesterday.
|
||||||
thresholds = TibberPricesThresholdConfig(
|
thresholds = TibberPricesThresholdConfig(
|
||||||
threshold_low=threshold_low,
|
threshold_low=threshold_low,
|
||||||
threshold_high=threshold_high,
|
threshold_high=threshold_high,
|
||||||
|
|
@ -205,7 +205,6 @@ def calculate_periods(
|
||||||
price_context,
|
price_context,
|
||||||
thresholds,
|
thresholds,
|
||||||
time=time,
|
time=time,
|
||||||
config_entry=config_entry,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -248,19 +248,21 @@ def add_interval_ends(periods: list[list[dict]], *, time: TibberPricesTimeServic
|
||||||
|
|
||||||
def filter_periods_by_end_date(periods: list[list[dict]], *, time: TibberPricesTimeService) -> list[list[dict]]:
|
def filter_periods_by_end_date(periods: list[list[dict]], *, time: TibberPricesTimeService) -> list[list[dict]]:
|
||||||
"""
|
"""
|
||||||
Filter periods to keep only relevant ones for today and tomorrow.
|
Filter periods to keep only relevant ones for yesterday, today, and tomorrow.
|
||||||
|
|
||||||
Keep periods that:
|
Keep periods that:
|
||||||
- End in the future (> now)
|
- End yesterday or later (>= start of yesterday)
|
||||||
- End today but after the start of the day (not exactly at midnight)
|
|
||||||
|
|
||||||
This removes:
|
This removes:
|
||||||
- Periods that ended yesterday
|
- Periods that ended before yesterday (day-before-yesterday or earlier)
|
||||||
- Periods that ended exactly at midnight today (they're completely in the past)
|
|
||||||
|
Rationale: Coordinator caches periods for yesterday/today/tomorrow so that:
|
||||||
|
- Binary sensors can filter for today+tomorrow (current/next periods)
|
||||||
|
- Services can access yesterday's periods when user requests "yesterday" data
|
||||||
"""
|
"""
|
||||||
now = time.now()
|
now = time.now()
|
||||||
today = now.date()
|
# Calculate start of yesterday (midnight yesterday)
|
||||||
midnight_today = time.start_of_local_day(now)
|
yesterday_start = time.start_of_local_day(now) - time.get_interval_duration() * 96 # 96 intervals = 24 hours
|
||||||
|
|
||||||
filtered = []
|
filtered = []
|
||||||
for period in periods:
|
for period in periods:
|
||||||
|
|
@ -274,13 +276,8 @@ def filter_periods_by_end_date(periods: list[list[dict]], *, time: TibberPricesT
|
||||||
if not period_end:
|
if not period_end:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Keep if period ends in the future
|
# Keep if period ends yesterday or later
|
||||||
if time.is_in_future(period_end):
|
if period_end >= yesterday_start:
|
||||||
filtered.append(period)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Keep if period ends today but AFTER midnight (not exactly at midnight)
|
|
||||||
if period_end.date() == today and period_end > midnight_today:
|
|
||||||
filtered.append(period)
|
filtered.append(period)
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
|
|
||||||
from .types import (
|
from .types import (
|
||||||
TibberPricesPeriodData,
|
TibberPricesPeriodData,
|
||||||
|
|
@ -16,7 +15,6 @@ if TYPE_CHECKING:
|
||||||
TibberPricesThresholdConfig,
|
TibberPricesThresholdConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import get_display_unit_factor
|
|
||||||
from custom_components.tibber_prices.utils.average import calculate_median
|
from custom_components.tibber_prices.utils.average import calculate_median
|
||||||
from custom_components.tibber_prices.utils.price import (
|
from custom_components.tibber_prices.utils.price import (
|
||||||
aggregate_period_levels,
|
aggregate_period_levels,
|
||||||
|
|
@ -29,7 +27,6 @@ def calculate_period_price_diff(
|
||||||
price_mean: float,
|
price_mean: float,
|
||||||
start_time: datetime,
|
start_time: datetime,
|
||||||
price_context: dict[str, Any],
|
price_context: dict[str, Any],
|
||||||
config_entry: ConfigEntry,
|
|
||||||
) -> tuple[float | None, float | None]:
|
) -> tuple[float | None, float | None]:
|
||||||
"""
|
"""
|
||||||
Calculate period price difference from daily reference (min or max).
|
Calculate period price difference from daily reference (min or max).
|
||||||
|
|
@ -37,10 +34,9 @@ def calculate_period_price_diff(
|
||||||
Uses reference price from start day of the period for consistency.
|
Uses reference price from start day of the period for consistency.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
price_mean: Mean price of the period (already in display units).
|
price_mean: Mean price of the period (in base currency).
|
||||||
start_time: Start time of the period.
|
start_time: Start time of the period.
|
||||||
price_context: Dictionary with ref_prices per day.
|
price_context: Dictionary with ref_prices per day.
|
||||||
config_entry: Config entry to get display unit configuration.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (period_price_diff, period_price_diff_pct) or (None, None) if no reference available.
|
Tuple of (period_price_diff, period_price_diff_pct) or (None, None) if no reference available.
|
||||||
|
|
@ -56,10 +52,9 @@ def calculate_period_price_diff(
|
||||||
if ref_price is None:
|
if ref_price is None:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# Convert reference price to display units
|
# Both prices are in base currency, no conversion needed
|
||||||
factor = get_display_unit_factor(config_entry)
|
ref_price_display = round(ref_price, 4)
|
||||||
ref_price_display = round(ref_price * factor, 2)
|
period_price_diff = round(price_mean - ref_price_display, 4)
|
||||||
period_price_diff = round(price_mean - ref_price_display, 2)
|
|
||||||
period_price_diff_pct = None
|
period_price_diff_pct = None
|
||||||
if ref_price_display != 0:
|
if ref_price_display != 0:
|
||||||
# CRITICAL: Use abs() for negative prices (same logic as calculate_difference_percentage)
|
# CRITICAL: Use abs() for negative prices (same logic as calculate_difference_percentage)
|
||||||
|
|
@ -96,23 +91,22 @@ def calculate_aggregated_rating_difference(period_price_data: list[dict]) -> flo
|
||||||
|
|
||||||
def calculate_period_price_statistics(
|
def calculate_period_price_statistics(
|
||||||
period_price_data: list[dict],
|
period_price_data: list[dict],
|
||||||
config_entry: ConfigEntry,
|
|
||||||
) -> dict[str, float]:
|
) -> dict[str, float]:
|
||||||
"""
|
"""
|
||||||
Calculate price statistics for a period.
|
Calculate price statistics for a period.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
period_price_data: List of price data dictionaries with "total" field.
|
period_price_data: List of price data dictionaries with "total" field.
|
||||||
config_entry: Config entry to get display unit configuration.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with price_mean, price_median, price_min, price_max, price_spread (all in display units).
|
Dictionary with price_mean, price_median, price_min, price_max, price_spread (all in base currency).
|
||||||
Note: price_spread is calculated based on price_mean (max - min range as percentage of mean).
|
Note: price_spread is calculated based on price_mean (max - min range as percentage of mean).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Convert prices to display units based on configuration
|
# Keep prices in base currency (Euro/NOK/SEK) for internal storage
|
||||||
factor = get_display_unit_factor(config_entry)
|
# Conversion to display units (ct/øre) happens in services/formatting layer
|
||||||
prices_display = [round(float(p["total"]) * factor, 2) for p in period_price_data]
|
factor = 1 # Always use base currency for storage
|
||||||
|
prices_display = [round(float(p["total"]) * factor, 4) for p in period_price_data]
|
||||||
|
|
||||||
if not prices_display:
|
if not prices_display:
|
||||||
return {
|
return {
|
||||||
|
|
@ -123,12 +117,12 @@ def calculate_period_price_statistics(
|
||||||
"price_spread": 0.0,
|
"price_spread": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
price_mean = round(sum(prices_display) / len(prices_display), 2)
|
price_mean = round(sum(prices_display) / len(prices_display), 4)
|
||||||
median_value = calculate_median(prices_display)
|
median_value = calculate_median(prices_display)
|
||||||
price_median = round(median_value, 2) if median_value is not None else 0.0
|
price_median = round(median_value, 4) if median_value is not None else 0.0
|
||||||
price_min = round(min(prices_display), 2)
|
price_min = round(min(prices_display), 4)
|
||||||
price_max = round(max(prices_display), 2)
|
price_max = round(max(prices_display), 4)
|
||||||
price_spread = round(price_max - price_min, 2)
|
price_spread = round(price_max - price_min, 4)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"price_mean": price_mean,
|
"price_mean": price_mean,
|
||||||
|
|
@ -224,14 +218,13 @@ def build_period_summary_dict(
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
def extract_period_summaries( # noqa: PLR0913
|
def extract_period_summaries(
|
||||||
periods: list[list[dict]],
|
periods: list[list[dict]],
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
price_context: dict[str, Any],
|
price_context: dict[str, Any],
|
||||||
thresholds: TibberPricesThresholdConfig,
|
thresholds: TibberPricesThresholdConfig,
|
||||||
*,
|
*,
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
config_entry: ConfigEntry,
|
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Extract complete period summaries with all aggregated attributes.
|
Extract complete period summaries with all aggregated attributes.
|
||||||
|
|
@ -253,7 +246,6 @@ def extract_period_summaries( # noqa: PLR0913
|
||||||
price_context: Dictionary with ref_prices and avg_prices per day.
|
price_context: Dictionary with ref_prices and avg_prices per day.
|
||||||
thresholds: Threshold configuration for calculations.
|
thresholds: Threshold configuration for calculations.
|
||||||
time: TibberPricesTimeService instance (required).
|
time: TibberPricesTimeService instance (required).
|
||||||
config_entry: Config entry to get display unit configuration.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from .types import ( # noqa: PLC0415 - Avoid circular import
|
from .types import ( # noqa: PLC0415 - Avoid circular import
|
||||||
|
|
@ -311,12 +303,12 @@ def extract_period_summaries( # noqa: PLR0913
|
||||||
thresholds.threshold_high,
|
thresholds.threshold_high,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate price statistics (in display units based on configuration)
|
# Calculate price statistics (in base currency, conversion happens in presentation layer)
|
||||||
price_stats = calculate_period_price_statistics(period_price_data, config_entry)
|
price_stats = calculate_period_price_statistics(period_price_data)
|
||||||
|
|
||||||
# Calculate period price difference from daily reference
|
# Calculate period price difference from daily reference
|
||||||
period_price_diff, period_price_diff_pct = calculate_period_price_diff(
|
period_price_diff, period_price_diff_pct = calculate_period_price_diff(
|
||||||
price_stats["price_mean"], start_time, price_context, config_entry
|
price_stats["price_mean"], start_time, price_context
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract prices for volatility calculation (coefficient of variation)
|
# Extract prices for volatility calculation (coefficient of variation)
|
||||||
|
|
|
||||||
|
|
@ -276,8 +276,9 @@ def calculate_periods_with_relaxation( # noqa: PLR0913, PLR0915 - Per-day relax
|
||||||
)
|
)
|
||||||
|
|
||||||
# === BASELINE CALCULATION (process ALL prices together, including yesterday) ===
|
# === BASELINE CALCULATION (process ALL prices together, including yesterday) ===
|
||||||
# Periods that ended yesterday will be filtered out later by filter_periods_by_end_date()
|
# Periods that ended before yesterday will be filtered out later by filter_periods_by_end_date()
|
||||||
baseline_result = calculate_periods(all_prices, config=config, time=time, config_entry=config_entry)
|
# This keeps yesterday/today/tomorrow periods in the cache
|
||||||
|
baseline_result = calculate_periods(all_prices, config=config, time=time)
|
||||||
all_periods = baseline_result["periods"]
|
all_periods = baseline_result["periods"]
|
||||||
|
|
||||||
# Count periods per day for min_periods check
|
# Count periods per day for min_periods check
|
||||||
|
|
@ -465,7 +466,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process ALL prices together (allows midnight crossing)
|
# Process ALL prices together (allows midnight crossing)
|
||||||
result = calculate_periods(all_prices, config=relaxed_config, time=time, config_entry=config_entry)
|
result = calculate_periods(all_prices, config=relaxed_config, time=time)
|
||||||
new_periods = result["periods"]
|
new_periods = result["periods"]
|
||||||
|
|
||||||
_LOGGER_DETAILS.debug(
|
_LOGGER_DETAILS.debug(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import DATA_CHART_METADATA_CONFIG, DOMAIN
|
from custom_components.tibber_prices.const import (
|
||||||
|
CONF_CURRENCY_DISPLAY_MODE,
|
||||||
|
DATA_CHART_METADATA_CONFIG,
|
||||||
|
DISPLAY_MODE_SUBUNIT,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -41,9 +46,11 @@ async def call_chartdata_service_for_metadata_async(
|
||||||
# Force metadata to "only" - this sensor ONLY provides metadata
|
# Force metadata to "only" - this sensor ONLY provides metadata
|
||||||
service_params["metadata"] = "only"
|
service_params["metadata"] = "only"
|
||||||
|
|
||||||
# Default to subunit_currency=True for ApexCharts compatibility (can be overridden in configuration.yaml)
|
# Use user's display unit preference from config_entry
|
||||||
|
# This ensures chart_metadata yaxis values match the user's configured currency display mode
|
||||||
if "subunit_currency" not in service_params:
|
if "subunit_currency" not in service_params:
|
||||||
service_params["subunit_currency"] = True
|
display_mode = config_entry.options.get(CONF_CURRENCY_DISPLAY_MODE, DISPLAY_MODE_SUBUNIT)
|
||||||
|
service_params["subunit_currency"] = display_mode == DISPLAY_MODE_SUBUNIT
|
||||||
|
|
||||||
# Call get_chartdata service using official HA service system
|
# Call get_chartdata service using official HA service system
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
DISPLAY_MODE_BASE,
|
DISPLAY_MODE_BASE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
format_price_unit_base,
|
||||||
get_display_unit_factor,
|
get_display_unit_factor,
|
||||||
get_display_unit_string,
|
get_display_unit_string,
|
||||||
)
|
)
|
||||||
|
|
@ -851,6 +852,11 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
||||||
if self.coordinator.data:
|
if self.coordinator.data:
|
||||||
currency = self.coordinator.data.get("currency")
|
currency = self.coordinator.data.get("currency")
|
||||||
|
|
||||||
|
# Special case: Energy Dashboard sensor always uses base currency
|
||||||
|
# regardless of user display mode configuration
|
||||||
|
if self.entity_description.key == "current_interval_price_base":
|
||||||
|
return format_price_unit_base(currency)
|
||||||
|
|
||||||
# Get unit based on user configuration (major or minor)
|
# Get unit based on user configuration (major or minor)
|
||||||
return get_display_unit_string(self.coordinator.config_entry, currency)
|
return get_display_unit_string(self.coordinator.config_entry, currency)
|
||||||
|
|
||||||
|
|
@ -861,7 +867,12 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
||||||
"""Check if the current time is within a best price period."""
|
"""Check if the current time is within a best price period."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return False
|
return False
|
||||||
attrs = get_price_intervals_attributes(self.coordinator.data, reverse_sort=False, time=self.coordinator.time)
|
attrs = get_price_intervals_attributes(
|
||||||
|
self.coordinator.data,
|
||||||
|
reverse_sort=False,
|
||||||
|
time=self.coordinator.time,
|
||||||
|
config_entry=self.coordinator.config_entry,
|
||||||
|
)
|
||||||
if not attrs:
|
if not attrs:
|
||||||
return False
|
return False
|
||||||
start = attrs.get("start")
|
start = attrs.get("start")
|
||||||
|
|
@ -876,7 +887,12 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
||||||
"""Check if the current time is within a peak price period."""
|
"""Check if the current time is within a peak price period."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return False
|
return False
|
||||||
attrs = get_price_intervals_attributes(self.coordinator.data, reverse_sort=True, time=self.coordinator.time)
|
attrs = get_price_intervals_attributes(
|
||||||
|
self.coordinator.data,
|
||||||
|
reverse_sort=True,
|
||||||
|
time=self.coordinator.time,
|
||||||
|
config_entry=self.coordinator.config_entry,
|
||||||
|
)
|
||||||
if not attrs:
|
if not attrs:
|
||||||
return False
|
return False
|
||||||
start = attrs.get("start")
|
start = attrs.get("start")
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ def aggregate_hourly_exact( # noqa: PLR0913, PLR0912, PLR0915
|
||||||
return hourly_data
|
return hourly_data
|
||||||
|
|
||||||
|
|
||||||
def get_period_data( # noqa: PLR0913, PLR0912, PLR0915, C901
|
def get_period_data( # noqa: PLR0913, PLR0912, PLR0915
|
||||||
*,
|
*,
|
||||||
coordinator: Any,
|
coordinator: Any,
|
||||||
period_filter: str,
|
period_filter: str,
|
||||||
|
|
@ -347,11 +347,12 @@ def get_period_data( # noqa: PLR0913, PLR0912, PLR0915, C901
|
||||||
# Median is more representative than mean for periods with gap tolerance
|
# Median is more representative than mean for periods with gap tolerance
|
||||||
# (single "normal" intervals between cheap/expensive ones don't skew the display)
|
# (single "normal" intervals between cheap/expensive ones don't skew the display)
|
||||||
price_median = period.get("price_median", 0.0)
|
price_median = period.get("price_median", 0.0)
|
||||||
# Convert to subunit currency if subunit_currency=True (periods stored in major)
|
# Convert to subunit currency if subunit_currency=True (periods stored in base currency)
|
||||||
if subunit_currency:
|
if subunit_currency:
|
||||||
price_median = price_median * 100
|
price_median = price_median * 100
|
||||||
if round_decimals is not None:
|
# Apply rounding: use round_decimals if provided, otherwise default precision
|
||||||
price_median = round(price_median, round_decimals)
|
precision = round_decimals if round_decimals is not None else (2 if subunit_currency else 4)
|
||||||
|
price_median = round(price_median, precision)
|
||||||
data_point[price_field] = price_median
|
data_point[price_field] = price_median
|
||||||
|
|
||||||
# Level (only if requested and present)
|
# Level (only if requested and present)
|
||||||
|
|
@ -373,11 +374,12 @@ def get_period_data( # noqa: PLR0913, PLR0912, PLR0915, C901
|
||||||
# 3. End time with NULL (cleanly terminate segment for ApexCharts)
|
# 3. End time with NULL (cleanly terminate segment for ApexCharts)
|
||||||
# Use price_median for consistency with sensor states (more representative for periods)
|
# Use price_median for consistency with sensor states (more representative for periods)
|
||||||
price_median = period.get("price_median", 0.0)
|
price_median = period.get("price_median", 0.0)
|
||||||
# Convert to subunit currency if subunit_currency=True (periods stored in major)
|
# Convert to subunit currency if subunit_currency=True (periods stored in base currency)
|
||||||
if subunit_currency:
|
if subunit_currency:
|
||||||
price_median = price_median * 100
|
price_median = price_median * 100
|
||||||
if round_decimals is not None:
|
# Apply rounding: use round_decimals if provided, otherwise default precision
|
||||||
price_median = round(price_median, round_decimals)
|
precision = round_decimals if round_decimals is not None else (2 if subunit_currency else 4)
|
||||||
|
price_median = round(price_median, precision)
|
||||||
start = period["start"]
|
start = period["start"]
|
||||||
end = period.get("end")
|
end = period.get("end")
|
||||||
start_serialized = start.isoformat() if hasattr(start, "isoformat") else start
|
start_serialized = start.isoformat() if hasattr(start, "isoformat") else start
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ from typing import TYPE_CHECKING, Any, Final
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import (
|
||||||
|
CONF_CURRENCY_DISPLAY_MODE,
|
||||||
|
DISPLAY_MODE_SUBUNIT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PRICE_LEVEL_CHEAP,
|
PRICE_LEVEL_CHEAP,
|
||||||
PRICE_LEVEL_EXPENSIVE,
|
PRICE_LEVEL_EXPENSIVE,
|
||||||
|
|
@ -32,7 +34,7 @@ from custom_components.tibber_prices.const import (
|
||||||
PRICE_RATING_HIGH,
|
PRICE_RATING_HIGH,
|
||||||
PRICE_RATING_LOW,
|
PRICE_RATING_LOW,
|
||||||
PRICE_RATING_NORMAL,
|
PRICE_RATING_NORMAL,
|
||||||
format_price_unit_subunit,
|
get_display_unit_string,
|
||||||
get_translation,
|
get_translation,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
@ -298,11 +300,15 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
# Get user's language from hass config
|
# Get user's language from hass config
|
||||||
user_language = hass.config.language or "en"
|
user_language = hass.config.language or "en"
|
||||||
|
|
||||||
# Get coordinator to access price data (for currency)
|
# Get coordinator to access price data (for currency) and config entry for display settings
|
||||||
_, coordinator, _ = get_entry_and_data(hass, entry_id)
|
config_entry, coordinator, _ = get_entry_and_data(hass, entry_id)
|
||||||
# Get currency from coordinator data
|
# Get currency from coordinator data
|
||||||
currency = coordinator.data.get("currency", "EUR")
|
currency = coordinator.data.get("currency", "EUR")
|
||||||
price_unit = format_price_unit_subunit(currency)
|
|
||||||
|
# Get user's display unit preference (subunit or base currency)
|
||||||
|
display_mode = config_entry.options.get(CONF_CURRENCY_DISPLAY_MODE, DISPLAY_MODE_SUBUNIT)
|
||||||
|
use_subunit = display_mode == DISPLAY_MODE_SUBUNIT
|
||||||
|
price_unit = get_display_unit_string(config_entry, currency)
|
||||||
|
|
||||||
# Get entity registry for mapping
|
# Get entity registry for mapping
|
||||||
entity_registry = async_get_entity_registry(hass)
|
entity_registry = async_get_entity_registry(hass)
|
||||||
|
|
@ -326,6 +332,54 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
(PRICE_LEVEL_VERY_EXPENSIVE, "#e74c3c"),
|
(PRICE_LEVEL_VERY_EXPENSIVE, "#e74c3c"),
|
||||||
]
|
]
|
||||||
series = []
|
series = []
|
||||||
|
|
||||||
|
# Get translated name for best price periods (needed for layer)
|
||||||
|
best_price_name = get_translation(["apexcharts", "best_price_period_name"], user_language) or "Best Price Period"
|
||||||
|
|
||||||
|
# Add best price period highlight overlay FIRST (so it renders behind all other series)
|
||||||
|
if highlight_best_price and entity_map:
|
||||||
|
# Create vertical highlight bands using separate Y-axis (0-1 range)
|
||||||
|
# This creates a semi-transparent overlay from bottom to top without affecting price scale
|
||||||
|
# Conditionally include day parameter (omit for rolling window mode)
|
||||||
|
# For rolling_window and rolling_window_autozoom, omit day parameter (dynamic selection)
|
||||||
|
day_param = "" if day in ("rolling_window", "rolling_window_autozoom", None) else f"day: ['{day}'], "
|
||||||
|
|
||||||
|
# Store original prices for tooltip, but map to 1 for full-height overlay
|
||||||
|
# Use user's display unit preference for period data too
|
||||||
|
subunit_param = "true" if use_subunit else "false"
|
||||||
|
best_price_generator = (
|
||||||
|
f"const response = await hass.callWS({{ "
|
||||||
|
f"type: 'call_service', "
|
||||||
|
f"domain: 'tibber_prices', "
|
||||||
|
f"service: 'get_chartdata', "
|
||||||
|
f"return_response: true, "
|
||||||
|
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
|
||||||
|
f"period_filter: 'best_price', "
|
||||||
|
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param} }} }}); "
|
||||||
|
f"const originalData = response.response.data; "
|
||||||
|
f"return originalData.map((point, i) => {{ "
|
||||||
|
f"const result = [point[0], point[1] === null ? null : 1]; "
|
||||||
|
f"result.originalPrice = point[1]; "
|
||||||
|
f"return result; "
|
||||||
|
f"}});"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use first entity from entity_map (reuse existing entity to avoid extra header entries)
|
||||||
|
best_price_entity = next(iter(entity_map.values()))
|
||||||
|
|
||||||
|
series.append(
|
||||||
|
{
|
||||||
|
"entity": best_price_entity,
|
||||||
|
"name": best_price_name,
|
||||||
|
"type": "area",
|
||||||
|
"color": "rgba(46, 204, 113, 0.05)", # Ultra-subtle green overlay (barely visible)
|
||||||
|
"yaxis_id": "highlight", # Use separate Y-axis (0-1) for full-height overlay
|
||||||
|
"show": {"legend_value": False, "in_header": False, "in_legend": False},
|
||||||
|
"data_generator": best_price_generator,
|
||||||
|
"stroke_width": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Only create series for levels that have a matching entity (filter out missing levels)
|
# Only create series for levels that have a matching entity (filter out missing levels)
|
||||||
for level_key, color in series_levels:
|
for level_key, color in series_levels:
|
||||||
# Skip levels that don't have a corresponding sensor
|
# Skip levels that don't have a corresponding sensor
|
||||||
|
|
@ -346,6 +400,8 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
|
|
||||||
# For rolling window modes, we'll capture metadata for dynamic config
|
# For rolling window modes, we'll capture metadata for dynamic config
|
||||||
# For static day modes, just return data array
|
# For static day modes, just return data array
|
||||||
|
# Use user's display unit preference for all data requests
|
||||||
|
subunit_param = "true" if use_subunit else "false"
|
||||||
if day in ("rolling_window", "rolling_window_autozoom", None):
|
if day in ("rolling_window", "rolling_window_autozoom", None):
|
||||||
data_generator = (
|
data_generator = (
|
||||||
f"const response = await hass.callWS({{ "
|
f"const response = await hass.callWS({{ "
|
||||||
|
|
@ -354,7 +410,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
f"service: 'get_chartdata', "
|
f"service: 'get_chartdata', "
|
||||||
f"return_response: true, "
|
f"return_response: true, "
|
||||||
f"service_data: {{ entry_id: '{entry_id}', {day_param}{filter_param}, "
|
f"service_data: {{ entry_id: '{entry_id}', {day_param}{filter_param}, "
|
||||||
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: true, "
|
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param}, "
|
||||||
f"connect_segments: true }} }}); "
|
f"connect_segments: true }} }}); "
|
||||||
f"return response.response.data;"
|
f"return response.response.data;"
|
||||||
)
|
)
|
||||||
|
|
@ -367,7 +423,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
f"service: 'get_chartdata', "
|
f"service: 'get_chartdata', "
|
||||||
f"return_response: true, "
|
f"return_response: true, "
|
||||||
f"service_data: {{ entry_id: '{entry_id}', {day_param}{filter_param}, "
|
f"service_data: {{ entry_id: '{entry_id}', {day_param}{filter_param}, "
|
||||||
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: true, "
|
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param}, "
|
||||||
f"connect_segments: true }} }}); "
|
f"connect_segments: true }} }}); "
|
||||||
f"return response.response.data;"
|
f"return response.response.data;"
|
||||||
)
|
)
|
||||||
|
|
@ -396,52 +452,6 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
# Note: Extrema markers don't work with data_generator approach
|
# Note: Extrema markers don't work with data_generator approach
|
||||||
# ApexCharts card requires direct entity data for extremas feature, not dynamically generated data
|
# ApexCharts card requires direct entity data for extremas feature, not dynamically generated data
|
||||||
|
|
||||||
# Get translated name for best price periods (needed for tooltip formatter)
|
|
||||||
best_price_name = get_translation(["apexcharts", "best_price_period_name"], user_language) or "Best Price Period"
|
|
||||||
|
|
||||||
# Add best price period highlight overlay (vertical bands from top to bottom)
|
|
||||||
if highlight_best_price and entity_map:
|
|
||||||
# Create vertical highlight bands using separate Y-axis (0-1 range)
|
|
||||||
# This creates a semi-transparent overlay from bottom to top without affecting price scale
|
|
||||||
# Conditionally include day parameter (omit for rolling window mode)
|
|
||||||
# For rolling_window and rolling_window_autozoom, omit day parameter (dynamic selection)
|
|
||||||
day_param = "" if day in ("rolling_window", "rolling_window_autozoom", None) else f"day: ['{day}'], "
|
|
||||||
|
|
||||||
# Store original prices for tooltip, but map to 1 for full-height overlay
|
|
||||||
# We use a custom tooltip formatter to show the real price
|
|
||||||
best_price_generator = (
|
|
||||||
f"const response = await hass.callWS({{ "
|
|
||||||
f"type: 'call_service', "
|
|
||||||
f"domain: 'tibber_prices', "
|
|
||||||
f"service: 'get_chartdata', "
|
|
||||||
f"return_response: true, "
|
|
||||||
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
|
|
||||||
f"period_filter: 'best_price', "
|
|
||||||
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: true }} }}); "
|
|
||||||
f"const originalData = response.response.data; "
|
|
||||||
f"return originalData.map((point, i) => {{ "
|
|
||||||
f"const result = [point[0], point[1] === null ? null : 1]; "
|
|
||||||
f"result.originalPrice = point[1]; "
|
|
||||||
f"return result; "
|
|
||||||
f"}});"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Use first entity from entity_map (reuse existing entity to avoid extra header entries)
|
|
||||||
best_price_entity = next(iter(entity_map.values()))
|
|
||||||
|
|
||||||
series.append(
|
|
||||||
{
|
|
||||||
"entity": best_price_entity,
|
|
||||||
"name": best_price_name,
|
|
||||||
"type": "area",
|
|
||||||
"color": "rgba(46, 204, 113, 0.05)", # Ultra-subtle green overlay (barely visible)
|
|
||||||
"yaxis_id": "highlight", # Use separate Y-axis (0-1) for full-height overlay
|
|
||||||
"show": {"legend_value": False, "in_header": False, "in_legend": False},
|
|
||||||
"data_generator": best_price_generator,
|
|
||||||
"stroke_width": 0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get translated title based on level_type
|
# Get translated title based on level_type
|
||||||
title_key = "title_rating_level" if level_type == "rating_level" else "title_level"
|
title_key = "title_rating_level" if level_type == "rating_level" else "title_level"
|
||||||
title = get_translation(["apexcharts", title_key], user_language) or (
|
title = get_translation(["apexcharts", title_key], user_language) or (
|
||||||
|
|
@ -506,15 +516,12 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
"shade": "light",
|
"shade": "light",
|
||||||
"type": "vertical",
|
"type": "vertical",
|
||||||
"shadeIntensity": 0.2,
|
"shadeIntensity": 0.2,
|
||||||
"opacityFrom": 0.7,
|
"opacityFrom": [0.5, 0.7, 0.7, 0.7, 0.7, 0.7],
|
||||||
"opacityTo": 0.25,
|
"opacityTo": 0.25,
|
||||||
|
"stops": [50, 100],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"dataLabels": {"enabled": False},
|
"dataLabels": {"enabled": False},
|
||||||
"tooltip": {
|
|
||||||
"x": {"format": "HH:mm"},
|
|
||||||
"y": {"title": {"formatter": f"function() {{ return '{price_unit}'; }}"}},
|
|
||||||
},
|
|
||||||
"legend": {
|
"legend": {
|
||||||
"show": False,
|
"show": False,
|
||||||
"position": "bottom",
|
"position": "bottom",
|
||||||
|
|
@ -522,22 +529,35 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
},
|
},
|
||||||
"grid": {
|
"grid": {
|
||||||
"show": True,
|
"show": True,
|
||||||
"borderColor": "#f5f5f5",
|
"borderColor": "rgba(144, 164, 174, 0.35)",
|
||||||
"strokeDashArray": 0,
|
"strokeDashArray": 0,
|
||||||
"xaxis": {"lines": {"show": False}},
|
"xaxis": {"lines": {"show": False}},
|
||||||
"yaxis": {"lines": {"show": True}},
|
"yaxis": {"lines": {"show": True}},
|
||||||
},
|
},
|
||||||
"markers": {
|
"markers": {
|
||||||
"size": 0, # No markers on data points
|
"size": 0, # No markers on data points
|
||||||
"hover": {"size": 2}, # Show marker only on hover
|
"hover": {"size": 3}, # Show marker only on hover
|
||||||
"strokeWidth": 1,
|
"colors": "#ff0000",
|
||||||
|
"fillOpacity": 0.5,
|
||||||
|
"strokeWidth": 5,
|
||||||
|
"strokeColors": "#ff0000",
|
||||||
|
"strokeOpacity": 0.15,
|
||||||
|
"showNullDataPoints": False,
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"enabled": True,
|
||||||
|
"enabledOnSeries": [1, 2, 3, 4, 5], # Enable for all price level series
|
||||||
|
"marker": {
|
||||||
|
"show": False,
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"show": False,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"yaxis": [
|
"yaxis": [
|
||||||
{
|
{
|
||||||
"id": "price",
|
"id": "price",
|
||||||
"decimals": 2,
|
|
||||||
"min": 0,
|
|
||||||
"apex_config": {"title": {"text": price_unit}},
|
"apex_config": {"title": {"text": price_unit}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -553,6 +573,9 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
if day == "rolling_window_autozoom"
|
if day == "rolling_window_autozoom"
|
||||||
else {"show": True, "color": "#8e24aa", "label": "🕒 LIVE"}
|
else {"show": True, "color": "#8e24aa", "label": "🕒 LIVE"}
|
||||||
),
|
),
|
||||||
|
"all_series_config": {
|
||||||
|
"float_precision": 2,
|
||||||
|
},
|
||||||
"series": series,
|
"series": series,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -632,7 +655,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
trigger_entities.append(current_price_sensor)
|
trigger_entities.append(current_price_sensor)
|
||||||
|
|
||||||
# Get metadata from chart_metadata sensor (preferred) or static fallback
|
# Get metadata from chart_metadata sensor (preferred) or static fallback
|
||||||
# The chart_metadata sensor provides yaxis_min, yaxis_max, and gradient_stop
|
# The chart_metadata sensor provides yaxis_min and yaxis_max
|
||||||
# as attributes, avoiding the need for async service calls in templates
|
# as attributes, avoiding the need for async service calls in templates
|
||||||
chart_metadata_sensor = next(
|
chart_metadata_sensor = next(
|
||||||
(
|
(
|
||||||
|
|
@ -662,18 +685,14 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
# Sensor not found - will show notification
|
# Sensor not found - will show notification
|
||||||
metadata_warning = True
|
metadata_warning = True
|
||||||
|
|
||||||
# Fixed gradient stop at 50% (visual appeal, no semantic meaning)
|
|
||||||
gradient_stops = [50, 100]
|
|
||||||
|
|
||||||
# Set fallback values if sensor not used
|
# Set fallback values if sensor not used
|
||||||
if not use_sensor_metadata:
|
if not use_sensor_metadata:
|
||||||
# Build yaxis config (only include min/max if not None)
|
# Build yaxis config (only include min/max if not None)
|
||||||
yaxis_price_config = {
|
yaxis_price_config = {
|
||||||
"id": "price",
|
"id": "price",
|
||||||
"decimals": 2,
|
|
||||||
"apex_config": {
|
"apex_config": {
|
||||||
"title": {"text": price_unit},
|
"title": {"text": price_unit},
|
||||||
"decimalsInFloat": 0,
|
"decimalsInFloat": 0 if use_subunit else 1,
|
||||||
"forceNiceScale": True,
|
"forceNiceScale": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -687,13 +706,12 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
# Build yaxis config with template variables
|
# Build yaxis config with template variables
|
||||||
yaxis_price_config = {
|
yaxis_price_config = {
|
||||||
"id": "price",
|
"id": "price",
|
||||||
"decimals": 2,
|
|
||||||
"min": "${v_yaxis_min}",
|
"min": "${v_yaxis_min}",
|
||||||
"max": "${v_yaxis_max}",
|
"max": "${v_yaxis_max}",
|
||||||
"apex_config": {
|
"apex_config": {
|
||||||
"title": {"text": price_unit},
|
"title": {"text": price_unit},
|
||||||
"decimalsInFloat": 0,
|
"decimalsInFloat": 0 if use_subunit else 1,
|
||||||
"forceNiceScale": False,
|
"forceNiceScale": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -735,10 +753,9 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
"shade": "light",
|
"shade": "light",
|
||||||
"type": "vertical",
|
"type": "vertical",
|
||||||
"shadeIntensity": 0.2,
|
"shadeIntensity": 0.2,
|
||||||
"opacityFrom": 0.7,
|
"opacityFrom": [0.5, 0.7, 0.7, 0.7, 0.7, 0.7],
|
||||||
"opacityTo": 0.25,
|
"opacityTo": 0.25,
|
||||||
"gradientToColors": ["#transparent"],
|
"stops": [50, 100],
|
||||||
"stops": gradient_stops,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -795,7 +812,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
template_value = f"states['{tomorrow_data_sensor}'].state === 'on' ? '+1d' : '+0d'"
|
template_value = f"states['{tomorrow_data_sensor}'].state === 'on' ? '+1d' : '+0d'"
|
||||||
|
|
||||||
# Get metadata from chart_metadata sensor (preferred) or static fallback
|
# Get metadata from chart_metadata sensor (preferred) or static fallback
|
||||||
# The chart_metadata sensor provides yaxis_min, yaxis_max, and gradient_stop
|
# The chart_metadata sensor provides yaxis_min and yaxis_max
|
||||||
# as attributes, avoiding the need for async service calls in templates
|
# as attributes, avoiding the need for async service calls in templates
|
||||||
chart_metadata_sensor = next(
|
chart_metadata_sensor = next(
|
||||||
(
|
(
|
||||||
|
|
@ -825,18 +842,14 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
# Sensor not found - will show notification
|
# Sensor not found - will show notification
|
||||||
metadata_warning = True
|
metadata_warning = True
|
||||||
|
|
||||||
# Fixed gradient stop at 50% (visual appeal, no semantic meaning)
|
|
||||||
gradient_stops = [50, 100]
|
|
||||||
|
|
||||||
# Set fallback values if sensor not used
|
# Set fallback values if sensor not used
|
||||||
if not use_sensor_metadata:
|
if not use_sensor_metadata:
|
||||||
# Build yaxis config (only include min/max if not None)
|
# Build yaxis config (only include min/max if not None)
|
||||||
yaxis_price_config = {
|
yaxis_price_config = {
|
||||||
"id": "price",
|
"id": "price",
|
||||||
"decimals": 2,
|
|
||||||
"apex_config": {
|
"apex_config": {
|
||||||
"title": {"text": price_unit},
|
"title": {"text": price_unit},
|
||||||
"decimalsInFloat": 0,
|
"decimalsInFloat": 0 if use_subunit else 1,
|
||||||
"forceNiceScale": True,
|
"forceNiceScale": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -850,13 +863,12 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
# Build yaxis config with template variables
|
# Build yaxis config with template variables
|
||||||
yaxis_price_config = {
|
yaxis_price_config = {
|
||||||
"id": "price",
|
"id": "price",
|
||||||
"decimals": 2,
|
|
||||||
"min": "${v_yaxis_min}",
|
"min": "${v_yaxis_min}",
|
||||||
"max": "${v_yaxis_max}",
|
"max": "${v_yaxis_max}",
|
||||||
"apex_config": {
|
"apex_config": {
|
||||||
"title": {"text": price_unit},
|
"title": {"text": price_unit},
|
||||||
"decimalsInFloat": 0,
|
"decimalsInFloat": 0 if use_subunit else 1,
|
||||||
"forceNiceScale": False,
|
"forceNiceScale": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -900,10 +912,9 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
"shade": "light",
|
"shade": "light",
|
||||||
"type": "vertical",
|
"type": "vertical",
|
||||||
"shadeIntensity": 0.2,
|
"shadeIntensity": 0.2,
|
||||||
"opacityFrom": 0.7,
|
"opacityFrom": [0.5, 0.7, 0.7, 0.7, 0.7, 0.7],
|
||||||
"opacityTo": 0.25,
|
"opacityTo": 0.25,
|
||||||
"gradientToColors": ["#transparent"],
|
"stops": [50, 100],
|
||||||
"stops": gradient_stops,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ from custom_components.tibber_prices.const import (
|
||||||
format_price_unit_base,
|
format_price_unit_base,
|
||||||
format_price_unit_subunit,
|
format_price_unit_subunit,
|
||||||
get_currency_info,
|
get_currency_info,
|
||||||
|
get_currency_name,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator.helpers import (
|
from custom_components.tibber_prices.coordinator.helpers import (
|
||||||
get_intervals_for_day_offsets,
|
get_intervals_for_day_offsets,
|
||||||
|
|
@ -97,6 +98,7 @@ def _calculate_metadata( # noqa: PLR0912, PLR0913, PLR0915
|
||||||
currency_obj = {
|
currency_obj = {
|
||||||
"code": currency,
|
"code": currency,
|
||||||
"symbol": base_symbol,
|
"symbol": base_symbol,
|
||||||
|
"name": get_currency_name(currency), # Full currency name (e.g., "Euro")
|
||||||
"unit": format_price_unit_base(currency),
|
"unit": format_price_unit_base(currency),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,13 +156,15 @@ def _calculate_metadata( # noqa: PLR0912, PLR0913, PLR0915
|
||||||
|
|
||||||
# Position precision: 2 decimals for subunit currency, 4 for base currency
|
# Position precision: 2 decimals for subunit currency, 4 for base currency
|
||||||
position_decimals = 2 if subunit_currency else 4
|
position_decimals = 2 if subunit_currency else 4
|
||||||
|
# Price precision: 2 decimals for subunit currency, 4 for base currency
|
||||||
|
price_decimals = 2 if subunit_currency else 4
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"min": round(min_val, 2),
|
"min": round(min_val, price_decimals),
|
||||||
"max": round(max_val, 2),
|
"max": round(max_val, price_decimals),
|
||||||
"avg": round(avg_val, 2),
|
"avg": round(avg_val, price_decimals),
|
||||||
"avg_position": round(avg_position, position_decimals),
|
"avg_position": round(avg_position, position_decimals),
|
||||||
"median": round(median_val, 2),
|
"median": round(median_val, price_decimals),
|
||||||
"median_position": round(median_position, position_decimals),
|
"median_position": round(median_position, position_decimals),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue