Commit graph

75 commits

Author SHA1 Message Date
Julian Pawlowski
c3173a16d6 refactor(attributes): streamline phase type retrieval and attribute building
Consolidate logic for determining current price phase and associated attributes by introducing shared helper functions. This enhances code maintainability and reduces duplication across components.

Impact: Improved clarity and efficiency in price phase handling for users.
2026-04-17 08:52:17 +00:00
Julian Pawlowski
7629c0f628 refactor(repairs): simplify currency mode change notification to one-shot
Remove the DATA_STATISTICS_REVIEW_REQUIRED flag and all associated
persistence logic. The flag approach was over-engineered: we cannot
detect whether the Recorder statistics have been fixed, and requiring
the user to re-save display settings as acknowledgement is bad UX.

New design: show the repair notice once when the mode changes.
The user dismisses it when done reviewing. The HA Recorder will
independently show its own unit-change dialog — that is sufficient.

Changes:
- Remove DATA_STATISTICS_REVIEW_REQUIRED constant from const.py
- Remove _check_statistics_review_repair() from __init__.py
- Remove ir import from __init__.py (no longer needed there)
- Remove flag set/clear logic from options_flow.py
- Change is_persistent=False (no restart persistence needed)
- Update all 5 translations: restore simple "Dismiss this notice" ending
2026-04-15 10:00:59 +00:00
Julian Pawlowski
1d065b11cd fix(services): use injected now in resolve_search_range day offset
_resolve_time_with_day_offset() was calling dt_util.now() internally
instead of using the injected now parameter. This caused incorrect date
calculations in tests and any caller that passes a specific reference time.

Also add missing price_rank_* sensor keys to TIME_SENSITIVE_ENTITY_KEYS
in coordinator/constants.py so quarter-hour refresh is registered for all
11 price rank sensors (current/next/previous interval and hour variants).

Rename dt as dt_utils → dt as dt_util (ICN001) across 11 files to follow
the project-wide import alias convention. Apply ruff auto-fixes for import
ordering and collapsing single-item imports throughout the codebase.

Released-Bug: no
2026-04-14 19:33:24 +00:00
Julian Pawlowski
a4ad506e01 feat(sensor): use dynamic precision for price rounding and display
Add get_display_precision() to const.py returning DISPLAY_PRECISION_SUBUNIT (2)
or DISPLAY_PRECISION_BASE (4) based on config. Replace hardcoded round(..., 2)
with get_display_precision() in all calculators and attribute builders.
Add _update_suggested_precision() to sensor core; syncs entity registry
suggested_display_precision on every coordinator update.

Interval price sensors get full precision (2 or 4 dp); other MONETARY sensors
get half precision (1 or 2 dp) as sensible default.

Impact: Price sensor states and attributes now correctly use 4 decimal places
in base-currency mode (was always 2). Display precision in dashboards updates
automatically when currency mode changes.
2026-04-14 19:28:19 +00:00
Julian Pawlowski
71696380a6 refactor(definitions): remove diagnostic entity category from day pattern sensors
Some checks failed
Auto-Tag on Version Bump / Check and create version tag (push) Waiting to run
Lint / Ruff (push) Waiting to run
Validate / Hassfest validation (push) Waiting to run
Validate / HACS validation (push) Waiting to run
Deploy Docusaurus Documentation (Dual Sites) / Build and Deploy Documentation Sites (push) Has been cancelled
Updated sensor definitions to enhance clarity and maintain consistency by removing the diagnostic entity category from day pattern sensors.

Impact: No user-facing changes.
2026-04-12 20:37:53 +00:00
Julian Pawlowski
4ba159d815 feat(translations): enhance price trend change descriptions and usage tips
Updated the long descriptions and usage tips for the price trend change sensors in multiple languages (de, en, nb, nl, sv) to provide clearer guidance on detection mechanics and expected behavior during V-shaped price days.

Impact: Users will have a better understanding of how the sensors operate and can make more informed decisions regarding automation based on price trends.
2026-04-12 19:55:57 +00:00
Julian Pawlowski
51a62d712f feat(sensor): add next/previous/rolling-hour price rank sensors
Rename the three existing price rank sensors from price_rank_* to
current_interval_price_rank_* to clarify they rank the current
quarter-hour interval's price, not a daily aggregate — consistent with
current_interval_price_level / current_interval_price_rating naming.

Add 8 new rank sensors covering additional subjects and reference windows:
- next_interval_price_rank_{today,today_tomorrow}
- previous_interval_price_rank_{today,today_tomorrow}
- current_hour_price_rank_{today,today_tomorrow}   (5-interval rolling avg)
- next_hour_price_rank_{today,today_tomorrow}       (5-interval rolling avg)

All new sensors are disabled by default. The volatility calculator gains a
subject parameter (_get_subject_price / _get_subject_price_attr_key /
_get_rolling_hour_avg_price) to select which price to rank. Sensor key
routing in value_getters.py and attributes/__init__.py updated accordingly.

No migration entries needed — the original price_rank_* sensors were never
released to users.

All 5 translation files updated. sensor-reference.md regenerated (129 entities).

Impact: Users can now track price rank for the next interval (look-ahead),
the previous interval (logging), and rolling hourly averages — for both
same-day and two-day reference windows.
2026-04-12 15:02:27 +00:00
Julian Pawlowski
6f5261785b feat(sensor): add price rank sensors and IQR-based volatility attributes
Add three new price rank sensors that show where today's/tomorrow's/combined
average price falls relative to all intervals in the evaluated window:
- price_rank_today: today's average price percentile rank (0–100%)
- price_rank_tomorrow: tomorrow's average price percentile rank
- price_rank_today_tomorrow: combined today+tomorrow percentile rank

Extend all volatility sensors with IQR-based band statistics:
- price_typical_spread: interquartile range (IQR) in currency subunit
- price_typical_spread_%: IQR as percentage of daily average
- price_spike_count: number of intervals outside Tukey fences (outliers)

Add calculate_iqr_stats() utility function in utils/price.py that computes
the 25th/75th percentiles, IQR, outer fences (Q1 - 1.5×IQR / Q3 + 1.5×IQR),
and outlier count for any list of price values. Entity keys and attribute
names use plain language (`price_rank`, `price_typical_spread`) as primary
labels; technical terms (percentile rank, IQR) are included parenthetically
in descriptions and documentation.

Impact: Users can now see where current day prices rank compared to their window and how tightly clustered or spike-prone a day's prices are.
2026-04-12 14:13:47 +00:00
Julian Pawlowski
1d3c55097d fix(periods): rename periods_remaining to period_count_remaining
Consistent naming with the period_count_* family introduced in the
previous commit (period_count_total, period_count_today,
period_count_tomorrow).

periods_remaining was the last attribute in the navigation triplet
using the old plural form. Renamed to period_count_remaining to follow
the established pattern: all countable period metrics use the
period_count_* prefix.

BREAKING CHANGE: periods_remaining renamed to period_count_remaining.

Impact: All four period count attributes now share the same prefix
(period_count_total, period_count_today, period_count_tomorrow,
period_count_remaining), making automation templates more predictable.
2026-04-12 10:05:21 +00:00
Julian Pawlowski
779e22a84e fix(periods): replace redundant total attributes with per-day counts
Some checks are pending
Deploy Docusaurus Documentation (Dual Sites) / Build and Deploy Documentation Sites (push) Waiting to run
Lint / Ruff (push) Waiting to run
Validate / Hassfest validation (push) Waiting to run
Validate / HACS validation (push) Waiting to run
Removed periods_found_total and replaced with period_count_today /
period_count_tomorrow. The old attribute counted all periods including
yesterday (coordinator scope), causing a discrepancy vs. the displayed
list (sensor scope, today+tomorrow only).

Renamed periods_total → period_count_total for naming consistency with
the new per-day attributes. Recalculate period_position / period_count_total /
periods_remaining after the today+tomorrow filter so all three navigation
attributes reflect the filtered scope.

period_count_tomorrow is always present (0 when no tomorrow data or no
periods found), enabling automations without default(0) guards.

Removed internal periods_found key from relaxation metadata — it was
only consumed by add_calculation_summary_attributes which is now removed.

BREAKING CHANGE: periods_found_total removed (replace with
period_count_today + period_count_tomorrow). periods_total renamed to
period_count_total.

Impact: Period navigation attributes (position/total/remaining) now
correctly reflect today+tomorrow scope. Per-day counts allow automations
to distinguish "2 periods today, 0 tomorrow" from "1+1".
2026-04-12 09:50:31 +00:00
Julian Pawlowski
447dc907e6 feat(sensors): add day pattern detection sensors (valley/peak/flat/rising/falling)
Introduces a new day_pattern.py module that analyses the 15-min price curve
for each calendar day (yesterday/today/tomorrow) and classifies its shape.

New sensors:
  day_pattern_yesterday / day_pattern_today / day_pattern_tomorrow
  EntityCategory.DIAGNOSTIC, SensorDeviceClass.ENUM

Patterns: valley, peak, double_valley, double_peak, flat, rising, falling, mixed

The detector uses centred-rolling smoothing, prominence-filtered extrema,
Kneedle-based knee detection, and monotone segment building.
Coordinator populates transformed_data["dayPatterns"] after priceInfo enrichment.

Impact: Users can trigger automations based on the shape of the day's price
curve, e.g. pre-heat when tomorrow is a valley day.
2026-04-11 21:07:16 +00:00
Julian Pawlowski
2f704a35a3 refactor: remove dead code across integration
Remove unused functions, constants, and entity definitions that were
left over from previous refactorings. All removed code was either
superseded by better implementations or never actually called.

Removed functions:
- entity_utils/helpers.py: translate_level(), translate_rating_level()
  (HA handles ENUM translation automatically via translations/*.json)
- entity_utils/attributes.py: build_timestamp_attribute(),
  build_period_attributes() (superseded by inline implementations)
- sensor/helpers.py: get_hourly_price_value(), aggregate_window_data()
  (replaced by Calculator Pattern in sensor/calculators/)

Removed constants and definitions:
- const.py: CONF_CHART_DATA_CONFIG (DATA_CHART_CONFIG is the active one),
  PRICE_LEVEL_OPTIONS, PRICE_RATING_OPTIONS, VOLATILITY_OPTIONS,
  PRICE_TREND_OPTIONS (never imported; options defined inline in
  definitions.py due to HA import timing constraints),
  async_get_home_type_translation() (sync version used instead)
- coordinator/core.py: FRESH_TO_CACHED_SECONDS (leftover from old
  caching strategy, never referenced)
- switch/definitions.py: BEST_PRICE_SWITCH_ENTITIES (duplicate of
  BEST_PRICE_SWITCH_ENTITY_DESCRIPTIONS using base class instead of
  custom TibberPricesSwitchEntityDescription subclass)

Cleanup:
- entity_utils/__init__.py: Remove exports for deleted functions
- sensor/helpers.py: Remove now-unused imports (timedelta,
  get_intervals_for_day_offsets, get_price_value, Callable)
- entity_utils/helpers.py: Remove unused get_price_level_translation
  import after translate_level() removal
- sensor/definitions.py: Update 7x "Keep in sync with *_OPTIONS"
  comments to reference individual PRICE_LEVEL_*/PRICE_RATING_*/
  VOLATILITY_* constants instead

Impact: No user-visible changes. Reduces codebase by ~130 lines.
Improves maintainability by eliminating misleading dead code.
2026-04-11 12:13:26 +00:00
Julian Pawlowski
07117801d2 fix(docs): correct inaccuracies and add missing documentation
Documentation fixes:
- configuration.md: Fix default min period length (30→60 min)
- configuration.md: Fix Average Sensor Display location (Step 6→General Settings)
- sensors-volatility.md: Add missing price_volatility attribute to table
- sensors-trends.md: Document undocumented next_price_trend_change_in sensor
- actions.md: Document undocumented get_price service

Code quality fixes:
- get_price.py: Fix misleading module docstring
- timing.py: Remove dead code (unreachable return None)
- binary_sensor/core.py: Simplify redundant tomorrow_data logic

Impact: Users get accurate documentation. No behavioral changes.
2026-04-11 11:51:52 +00:00
Julian Pawlowski
ac7cd5b572 fix(lint): apply Python 3.14 ruff rules and update HA minimum version
Add UP037 to ruff ignore list to preserve quoted TYPE_CHECKING forward
references (PEP 649 lazy eval breaks get_type_hints() at runtime for
TYPE_CHECKING-guarded imports).

Move datetime imports into TYPE_CHECKING blocks in sensor/calculators
timing.py and trend.py (TC003, type-only usage confirmed).

Apply PEP 758 parenthesis-free except clauses across 7 files via
ruff format with target-version=py314.

Update hacs.json minimum HA version to 2026.4.0, the first HA release
requiring Python 3.14.

Impact: Linter config now correctly handles Python 3.14 semantics.
Users need HA >= 2026.4 (Python 3.14) to use this integration.
2026-04-11 10:56:34 +00:00
Julian Pawlowski
2a08515ba0 fix(sensors): use consistent rounding for trend duration calculations
Some checks are pending
Lint / Ruff (push) Waiting to run
Validate / Hassfest validation (push) Waiting to run
Validate / HACS validation (push) Waiting to run
Replaced int(time.minutes_until()) with time.minutes_until_rounded()
in trend calculator (3 locations). The int() call truncated values
(14.7 → 14) while timing sensors used standard rounding (14.7 → 15).

All duration sensors now use the same rounding method
(math.floor(seconds/60 + 0.5)), matching HA's timestamp rendering
behavior.

Impact: Trend countdown values may differ by ±1 minute compared to
previous behavior. Consistency across all duration sensors improved.
2026-04-10 09:13:32 +00:00
Julian Pawlowski
faa3b2b71a feat(sensors)!: use native minutes for all duration sensors
Changed native_unit_of_measurement from HOURS to MINUTES for all 7
duration sensors. HA auto-converts to hours for display via
suggested_unit_of_measurement=HOURS.

Sensors affected:
- next_price_trend_change_in
- best_price_period_duration, best_price_remaining_minutes,
  best_price_next_in_minutes
- peak_price_period_duration, peak_price_remaining_minutes,
  peak_price_next_in_minutes

Removed _minutes_to_hours() conversion function — calculator values
(minutes) are now passed through directly.

BREAKING CHANGE: State values for all duration sensors change from
hours to minutes (e.g. 1.5 → 90). The display unit remains hours
(suggested_unit_of_measurement). Automations using numeric state
comparisons must be updated (multiply old thresholds by 60).

Impact: Users with automations comparing duration sensor states
numerically need to update thresholds. Dashboard display is unchanged
for new installations. Existing installations retain their configured
display unit but the underlying numeric value changes.
2026-04-10 09:08:38 +00:00
Julian Pawlowski
b1b41be9aa feat(sensors)!: rename trend change countdown sensor for naming consistency
Renamed trend_change_in_minutes → next_price_trend_change_in to align
with its sibling sensor next_price_trend_change (timestamp variant).

Follows the established best/peak price naming pattern where related
sensors share a common prefix (e.g. best_price_next_start_time /
best_price_next_in_minutes).

Updated entity key, translation key, friendly names (all 5 languages),
custom translations, coordinator constants, attribute routing, and
cache-clear mapping.

BREAKING CHANGE: Entity ID changes from
sensor.<home>_trend_change_in_minutes to
sensor.<home>_next_price_trend_change_in. Automations and dashboards
referencing the old entity ID must be updated.

Impact: Users with automations or dashboard cards referencing the old
sensor name need to update references. The sensor retains identical
functionality and attributes.
2026-04-10 09:08:27 +00:00
Julian Pawlowski
459d6762c7 perf(sensors): add call-avoidance for timer-based state updates
Skip expensive async_write_ha_state() when native_value hasn't changed
since last write. HA's state machine has built-in change detection, but
it only runs AFTER all properties and attributes are evaluated — the
expensive part we now avoid entirely.

Sensor platform (Timer #2 + #3):
- New _write_if_changed() method compares native_value before writing
- Timer #3 (30s, 7 entities): Skips all writes when no period active
- Timer #2 (15min, ~45 entities): Skips enum levels/ratings that stay
  constant across quarter-hour intervals
- Replaces data_lifecycle_status-only pattern with unified approach

Binary sensor platform (Timer #2):
- Period sensors only write at actual period boundaries, not every 15min

Coordinator push updates always write (sentinel reset ensures freshness).

Impact: Eliminates asyncio "Executing TimerHandle took 1.4s" warnings
caused by redundant property evaluation in Timer #3 callbacks. Reduces
event loop blocking from ~1.4s to microseconds when values unchanged.
2026-04-09 19:04:04 +00:00
Julian Pawlowski
edabb49309 feat(sensors): expose energy/tax breakdown as sensor attributes
Add energy_price and tax attributes to interval and daily stat sensors:

- Interval sensors (current/next/previous): energy_price and tax from
  the specific 15-minute interval
- Daily min/max sensors: energy_price and tax from the extreme interval
- Daily average sensors: energy_price_mean, energy_price_median,
  tax_mean, tax_median — matching the existing mean/median pattern
  used for the main price attribute

Calculator caches both mean and median for energy/tax using
calculate_median() from utils/average. All new attributes are
excluded from Recorder to prevent database bloat.

Impact: Users can see price composition (spot price vs. taxes) on
all major price sensors. Enables solar feed-in and net metering
automations based on raw energy prices.
2026-04-09 18:27:36 +00:00
Julian Pawlowski
33f57ff077 feat(sensors)!: rename price_trend_Xh → price_outlook_Xh, add price_trajectory_Xh
Renamed 8 sensors to clarify what they actually measure, and added 7 new
sensors for a different (and often more useful) calculation.

--- WHY THE RENAME ---

The old name "price_trend_Xh" implied the sensor shows where prices are
heading. It doesn't — it compares CURRENT price vs the FUTURE WINDOW AVERAGE.
At a price minimum, it shows "strongly_falling" (because the cheap minimum
pulls the average below your current high price), which is the opposite of
intuitive. The name "price_outlook_Xh" correctly conveys: "is now cheaper
or more expensive than the next Nh on average?"

--- NEW: price_trajectory_Xh ---

These sensors compare FIRST HALF vs SECOND HALF of the window, revealing
actual price direction within the window:

  price_trajectory_2h: avg(hour 1) vs avg(hour 2)
  price_trajectory_3h: avg(first 1.5h) vs avg(second 1.5h)
  price_trajectory_4h: avg(first 2h) vs avg(second 2h)
  price_trajectory_5h: avg(first 2.5h) vs avg(second 2.5h)
  price_trajectory_6h: avg(first 3h) vs avg(second 3h)
  price_trajectory_8h: avg(first 4h) vs avg(second 4h)
  price_trajectory_12h: avg(first 6h) vs avg(second 6h)

The key use case: at a price minimum, price_outlook_Xh shows "strongly_falling"
but price_trajectory_Xh shows "rising" — correctly revealing the upcoming
reversal. "outlook: falling + trajectory: rising" = you're AT the minimum.

--- IMPLEMENTATION ---

sensor/calculators/trend.py:
  - get_price_outlook_value() (was: get_price_trend_value())
  - New: get_price_trajectory_value(*, hours: int)
  - New: _calculate_first_half_average(hours, next_interval_start)
  - New: get_trajectory_attributes() → first_half_avg, second_half_avg, half_diff_%
  - clear_trend_cache() also resets _trajectory_attributes

sensor/definitions.py:
  - 8 SensorEntityDescription entries: key/translation_key price_trend_Xh → price_outlook_Xh
  - New PRICE_TRAJECTORY_SENSORS tuple (2h–5h enabled by default, 6h/8h/12h disabled)

sensor/value_getters.py:
  - 8 lambda entries renamed
  - 7 new trajectory lambda entries added

sensor/attributes/trend.py:
  - startswith("price_trend_") → startswith("price_outlook_")
  - New elif branch routing price_trajectory_* to cached trajectory_attributes

sensor/core.py:
  - startswith checks updated for both prefix families
  - cached_data dict extended with "trajectory_attributes"

coordinator/constants.py:
  - TIME_SENSITIVE_ENTITY_KEYS: 8 renamed + 7 new trajectory keys added

config_flow_handlers/entity_check.py:
  - volatility + price_trend affected-entity lists: 8 renamed + 7 new

BREAKING CHANGE: Sensors price_trend_1h, price_trend_2h, price_trend_3h,
price_trend_4h, price_trend_5h, price_trend_6h, price_trend_8h,
price_trend_12h have been removed without a deprecation period.

Migration:
  Replace price_trend_Xh → price_outlook_Xh everywhere (automations,
  dashboards, templates). Behavior is identical — only the entity name
  changed. If you want to detect actual price direction within the window
  (e.g. "are prices rising or falling right now?"), use the new
  price_trajectory_Xh sensors instead.

Impact: Users must update automations and dashboards. Entity IDs change from
sensor.<home>_price_trend_Xh to sensor.<home>_price_outlook_Xh. New
price_trajectory_Xh sensors provide complementary direction information.
2026-04-09 16:08:42 +00:00
Julian Pawlowski
d0b6ea0e1a fix(sensors)!: fix DURATION sensors displaying in minutes instead of hours
Some checks failed
Validate / HACS validation (push) Has been cancelled
Lint / Ruff (push) Has been cancelled
Validate / Hassfest validation (push) Has been cancelled
Added `suggested_unit_of_measurement=UnitOfTime.HOURS` to all 7 DURATION
sensors to prevent HA from auto-selecting minutes as the display unit.
Without this, HA would pick "min" for small values (e.g., 0.75 h) and then
display large values as "1238 Min." instead of the intended "20 Std. 38 Min."

Affected sensors:
- trend_change_in_minutes
- best_price_period_duration / peak_price_period_duration
- best_price_remaining_minutes / peak_price_remaining_minutes
- best_price_next_in_minutes / peak_price_next_in_minutes

BREAKING CHANGE: Sensor state unit changes from minutes to hours for users
whose entity registry stored "min" as the display unit (the previous default).
Automations using the raw state value (e.g., `state < 60` for "less than 60
minutes") must be updated to use hours (e.g., `state < 1`).
The state attributes `remaining_minutes` and `next_in_minutes` continue to
provide integer minutes and are unaffected.

Impact: Duration sensors now display dynamically as "X h Y min" (e.g.,
"1 h 15 min") instead of a large minutes value like "1238 Min.". Users who
manually customized the unit in HA settings are not affected.
2026-04-08 08:01:16 +00:00
Julian Pawlowski
91efeed90f feat(sensors): add trend_change_in_minutes countdown sensor
New duration sensor showing time until next price trend change as hours
(e.g., 2.25 h). Registered in MINUTE_UPDATE_ENTITY_KEYS for per-minute
updates. Shares cached attributes with next_price_trend_change timestamp
sensor.

Added trend attributes to _unrecorded_attributes (threshold/volatility/diff
attributes excluded from recorder). Updated timer group size test expectation
from 6 to 7.

Impact: Users can display a live countdown to the next trend change on
dashboards and use it in automations (e.g., "if < 0.25 h, prepare").
2026-04-07 13:44:22 +00:00
Julian Pawlowski
90e2c3c1dc feat(trend): add direction-group detection, noise floor, and confirmation hysteresis
Refactored trend calculator with direction-group-based trend change detection
(rising/strongly_rising treated as same group, falling/strongly_falling as same
group). Added minimum absolute price change thresholds (noise floor) to prevent
spurious trends at low price levels. Both percentage AND absolute conditions
must now be met.

Updated strongly threshold defaults from ±6% to ±9% (3x base for perceptual
scaling). Added missing strongly thresholds and new config keys to
get_default_options(). calculate_price_trend() now returns volatility_factor
as 4th tuple element for threshold transparency.

Added CONF_PRICE_TREND_CHANGE_CONFIRMATION (default: 3 intervals = 45min)
and CONF_PRICE_TREND_MIN_PRICE_CHANGE / _STRONGLY with validation limits.

Updated tests for new 4-tuple return value.

Impact: More stable trend detection — fewer false trend changes during low-price
periods. Direction-group logic prevents noise from "rising ↔ strongly_rising"
oscillations. Users can fine-tune noise floor for their market.
2026-04-07 13:44:01 +00:00
Julian Pawlowski
636bd7a797 refactor(sensor): replace redundant pass-through lambdas with direct references
PLW0108: Three lambdas were simple pass-throughs that added no value:

  lambda data: aggregate_level_data(data)  →  aggregate_level_data
  lambda: lifecycle_calculator.get_lifecycle_state()  →  lifecycle_calculator.get_lifecycle_state

Affected files:
  sensor/calculators/rolling_hour.py (line 115)
  sensor/helpers.py (line 139)
  sensor/value_getters.py (line 220)

Impact: No behaviour change. Linter now passes with zero warnings.
2026-04-06 14:28:51 +00:00
Julian Pawlowski
5411a75b79 fix(sensor): set state_class=None on static diagnostic metadata sensors
Four non-MONETARY diagnostic sensors had state_class set, causing HA
Recorder to add them to long-term statistics tables unnecessarily:

- home_size (m²):                   SensorStateClass.MEASUREMENT
- main_fuse_size (A):               SensorStateClass.MEASUREMENT
- number_of_residents:              SensorStateClass.MEASUREMENT
- estimated_annual_consumption(kWh):SensorStateClass.TOTAL

All four are static user metadata retrieved from Tibber's user API
(cached for 24 h, rarely or never changes in practice).  They carry no
time-series value: home_size and main_fuse_size don't change, and
estimated_annual_consumption is a rough Tibber estimate, not an actual
accumulating energy counter.

Setting state_class=None removes them from long-term statistics while
keeping normal state-change recording intact.

The three intentional non-None state_class values are unchanged:
- current_interval_price (MONETARY, TOTAL): Energy Dashboard
- current_interval_price_base (MONETARY, TOTAL): Energy Dashboard
- average_price_today (MONETARY, TOTAL): useful weekly/monthly trend

Impact: Reduced Recorder database growth; no user-visible sensor
behaviour change.
2026-04-06 14:24:02 +00:00
Julian Pawlowski
f8fd0f4936 refactor(entities): remove redundant name= from all entity descriptions
All entity descriptions had hardcoded English name= strings that were
never used at runtime: HA always prefers translations via translation_key
(entity.<platform>.<key>.name in translations/en.json), making the name=
fields dead code.

Removed 106 lines across all four platforms:
- sensor/definitions.py: 85 name= lines
- number/definitions.py: 12 name= lines
- binary_sensor/definitions.py: 6 name= lines
- switch/definitions.py: 3 name= lines

No functional change. The unique_id (entry_id + key) and translation_key
remain stable, ensuring entity IDs and friendly names are unaffected.

Impact: Cleaner definitions, no drift between name= strings and
translations. Aligns with HA standard: translations are the single
source of truth for entity names.
2026-04-06 13:16:07 +00:00
Julian Pawlowski
807b670ff5 perf(sensors): reduce long-term statistics to 3 MONETARY sensors
Previously all 26 MONETARY sensors had state_class=TOTAL, causing the
statistics and statistics_short_term tables to grow unbounded (never
auto-purged by HA).

Reduced to 3 sensors that genuinely benefit from long-term history:
- current_interval_price (main price sensor, trend over weeks/months)
- current_interval_price_base (required for Energy Dashboard)
- average_price_today (daily avg tracking over seasons)

Set state_class=None on 23 sensors where long-term history adds no
value: forecast/future sensors (next_avg_*h), daily snapshots
(lowest/highest_price_today), tomorrow sensors, rolling windows
(trailing/leading 24h), and next/previous interval sensors.

Note: state_class=None does not affect the States timeline (History
panel). Only the Statistics chart on entity detail pages is removed
for the affected sensors. Existing statistics data is retained.

Impact: ~88% reduction in statistics table writes. Prevents database
bloat reported by users with long-running installations.
2026-04-06 12:47:25 +00:00
Julian Pawlowski
6aa76affea fix(sensor): best price calculation on v-shaped days 2026-04-06 11:13:09 +00:00
Julian Pawlowski
5fc1f4db33 feat(sensors): add 5-level price trend scale with configurable thresholds
Extends trend sensors from 3-level (rising/stable/falling) to 5-level scale
(strongly_rising/rising/stable/falling/strongly_falling) for finer granularity.

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

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

Impact: Users get more nuanced trend information for automation decisions.
New trend_value attribute enables numeric comparisons (e.g., > 0 for any rise).
Existing automations using "rising"/"falling"/"stable" continue to work.
2026-01-20 13:36:01 +00:00
Julian Pawlowski
3e6bcf2345 fix(sensor): synchronize current_interval_price_base with current_interval_price
Fixed inconsistency between "Current Electricity Price" and "Current Electricity Price
(Energy Dashboard)" sensors that were showing different prices and icons.

Changes:
- Add current_interval_price_base to TIME_SENSITIVE_ENTITY_KEYS so it updates at
  quarter-hour boundaries instead of only on API polls. This ensures both sensors
  update synchronously when a new 15-minute interval starts.
- Use interval_data["startsAt"] as timestamp for current interval price sensors
  (both variants) instead of rounded calculation time. This prevents timestamp
  divergence when sensors update at slightly different times.
- Include current_interval_price_base in icon color attribute mapping so both
  sensors display the same dynamic cash icon based on current price level.
- Include current_interval_price_base in dynamic icon function so it gets the
  correct icon based on current price level (VERY_CHEAP/CHEAP/NORMAL/EXPENSIVE).

Impact: Both sensors now show identical prices, timestamps, and icons as intended.
They update synchronously at interval boundaries (00, 15, 30, 45 minutes) and
correctly represent the Energy Dashboard compatible variant without lag or
inconsistencies.
2025-12-26 16:23:05 +00:00
Julian Pawlowski
0a4af0de2f feat(sensor): convert timing sensors to hour-based display with minute attributes
Convert best_price and peak_price timing sensors to display in hours (UI-friendly)
while retaining minute values in attributes (automation-friendly). This improves
readability in dashboards by using Home Assistant's automatic duration formatting
"1 h 35 min" instead of decimal "1.58 h".

BREAKING CHANGE: State unit changed from minutes to hours for 6 timing sensors.

Affected sensors:
  * best_price_period_duration, best_price_remaining_minutes, best_price_next_in_minutes
  * peak_price_period_duration, peak_price_remaining_minutes, peak_price_next_in_minutes

Migration guide for users:
  - If your automations use {{ state_attr(..., 'remaining_time') }} or similar:
    No action needed - attribute values remain in minutes
  - If your automations use {{ states('sensor.best_price_remaining_minutes') }} directly:
    Update to use the minute attribute instead: {{ state_attr('sensor.best_price_remaining_minutes', 'remaining_minutes') }}
  - If your dashboards display the state value:
    Values now show as "1 h 35 min" instead of "95" - this is the intended improvement
  - If your templates do math with the state: multiply by 60 to convert hours back to minutes
    Before: remaining * 60
    After: remaining_minutes (use attribute directly)

Implementation details:
- Timing sensors now use device_class=DURATION, unit=HOURS, precision=2
- State values converted from minutes to hours via _minutes_to_hours()
- New minute-precision attributes added for automation compatibility:
  * period_duration_minutes (for checking if period is long enough)
  * remaining_minutes (for countdown-based automation logic)
  * next_in_minutes (for time-to-event automation triggers)
- Translation improvements across all 5 languages (en, de, nb, nl, sv):
  * Descriptions now clarify state in hours vs attributes in minutes
  * Long descriptions explain dual-format architecture
  * Usage tips updated to reference minute attributes for automations
  * All translation files synchronized (fixed order, removed duplicates)
- Type safety: Added type assertions (cast) for timing calculator results to
  satisfy Pyright type checking (handles both float and datetime return types)

Home Assistant now automatically formats these durations as "1 h 35 min" for improved
UX, matching the behavior of battery.remaining_time and other duration sensors.

Rationale for breaking change:
The previous minute-based state was unintuitive for users ("95 minutes" doesn't
immediately convey "1.5 hours") and didn't match Home Assistant's standard duration
formatting. The new hour-based state with minute attributes provides:
- Better UX: Automatic "1 h 35 min" formatting in UI
- Full automation compatibility: Minute attributes for all calculation needs
- Consistency: Matches HA's duration sensor pattern (battery, timer, etc.)

Impact: Timing sensors now display in human-readable hours with full backward
compatibility via minute attributes. Users relying on direct state access must
migrate to minute attributes (simple change, documented above).
2025-12-26 16:03:00 +00:00
Julian Pawlowski
09a50dccff fix(sensor): streamline lifecycle attrs and next poll visibility
- Remove pool stats/fetch-age from lifecycle sensor to avoid stale data under state-change filtering; add `next_api_poll` for transparency.
- Clean lifecycle calculator by dropping unused helpers/constants and delete the obsolete cache age test.
- Clarify lifecycle state is diagnostics-only in coordinator comments, keep state-change filtering in timer test, and retain quarter-hour precision notes in constants.
- Keep sensor core aligned with lifecycle state filtering.

Impact: Lifecycle sensor now exposes only state-relevant fields without recorder noise, next API poll is visible, and dead code/tests tied to removed attributes are gone.
2025-12-26 12:13:36 +00:00
Julian Pawlowski
c6d6e4a5b2 fix(volatility): expose price coefficient variation attribute
Expose the `price_coefficient_variation_%` value across period statistics, binary sensor attributes, and the volatility calculator, and refresh the volatility descriptions/translations to mention the coefficient-of-variation metric.
2025-12-25 19:10:42 +00:00
Julian Pawlowski
78df8a4b17 refactor(lifecycle): integrate with Pool for sensor metrics
Replace cache-based metrics with Pool as single source of truth:
- get_cache_age_minutes() → get_sensor_fetch_age_minutes() (from Pool)
- Remove get_cache_validity_status(), get_data_completeness_status()
- Add get_pool_stats() for comprehensive pool statistics
- Add has_tomorrow_data() using Pool as source

Attributes now show:
- sensor_intervals_count/expected/has_gaps (protected range)
- cache_intervals_total/limit/fill_percent/extra (entire pool)
- last_sensor_fetch, cache_oldest/newest_interval timestamps
- tomorrow_available based on Pool state

Impact: More accurate lifecycle status, consistent with Pool as source
of truth, cleaner diagnostic information.
2025-12-23 14:13:34 +00:00
Julian Pawlowski
49b8a018e7 fix(types): resolve Pyright type errors
- coordinator/core.py: Fix return type for _get_threshold_percentages()
- coordinator/data_transformation.py: Add type ignore for cached data return
- sensor/core.py: Initialize _state_info with required unrecorded_attributes
2025-12-22 23:22:02 +00:00
Julian Pawlowski
325d855997 feat(utils): add coefficient of variation (CV) calculation
Add calculate_coefficient_of_variation() as central utility function:
- CV = (std_dev / mean) * 100 as standardized volatility measure
- calculate_volatility_with_cv() returns both level and numeric CV
- Volatility sensors now expose CV in attributes for transparency

Used as foundation for quality gates, adaptive smoothing, and period statistics.

Impact: Volatility sensors show numeric CV percentage alongside categorical level,
enabling users to see exact price variation.
2025-12-22 23:21:38 +00:00
Julian Pawlowski
abb02083a7 feat(sensors): always show both mean and median in average sensor attributes
Implemented configurable display format (mean/median/both) while always
calculating and exposing both price_mean and price_median attributes.

Core changes:
- utils/average.py: Refactored calculate_mean_median() to always return both
  values, added comprehensive None handling (117 lines changed)
- sensor/attributes/helpers.py: Always include both attributes regardless of
  user display preference (41 lines)
- sensor/core.py: Dynamic _unrecorded_attributes based on display setting
  (55 lines), extracted helper methods to reduce complexity
- Updated all calculators (rolling_hour, trend, volatility, window_24h) to
  use new always-both approach

Impact: Users can switch display format in UI without losing historical data.
Automation authors always have access to both statistical measures.
2025-12-18 15:12:30 +00:00
Julian Pawlowski
6c741e8392 fix(config_flow): restructure options flow to menu-based navigation and fix settings persistence
Fixes configuration wizard not saving settings (#59):

Root cause was twofold:
1. Linear multi-step flow pattern didn't properly persist changes between steps
2. Best/peak price settings used nested sections format - values were saved
   in sections (period_settings, flexibility_settings, etc.) but read from
   flat structure, causing configured values to be ignored on subsequent runs

Solution:
- Replaced linear step-through flow with menu-based navigation system
- Each configuration area now has dedicated "Save & Back" buttons
- Removed nested sections from all steps except best/peak price (where they
  provide better UX for grouping related settings)
- Fixed best/peak price steps to correctly extract values from sections:
  period_settings, flexibility_settings, relaxation_and_target_periods
- Added reset-to-defaults functionality with confirmation dialog

UI/UX improvements:
- Menu structure: General Settings, Currency Display, Price Rating Thresholds,
  Volatility, Best Price Period, Peak Price Period, Price Trend,
  Chart Data Export, Reset to Defaults, Back
- Removed confusing step progress indicators ("{step_num} / {total_steps}")
- Changed all submit buttons from "Continue →" to "↩ Save & Back"
- Clear grouping of settings by functional area

Translation updates (nl.json + sv.json):
- Refined volatility threshold descriptions with CV formula explanations
- Clarified price trend thresholds (compares current vs. future N-hour average,
  not "per hour increase")
- Standardized terminology (e.g., "entry" → "item", compound word consistency)
- Consistently formatted all sensor names and descriptions
- Added new data lifecycle status sensor names

Technical changes:
- Options flow refactored from linear to menu pattern with menu_options dict
- New reset_to_defaults step with confirmation and abort handlers
- Section extraction logic in best_price/peak_price steps now correctly reads
  from nested structure (period_settings.*, flexibility_settings.*, etc.)
- Removed sections from general_settings, display_settings, volatility, etc.
  (simpler flat structure via menu navigation)

Impact: Configuration wizard now reliably saves all settings. Users can
navigate between setting areas without restarting the flow. Reset function
enables quick recovery when experimenting with thresholds. Previously
configured best/peak price settings are now correctly applied.
2025-12-13 13:33:31 +00:00
Julian Pawlowski
1c19cebff5 fix: support main and subunit currency 2025-12-11 23:07:06 +00:00
Julian Pawlowski
60e05e0815 refactor(currency)!: rename major/minor to base/subunit currency terminology
Complete terminology migration from confusing "major/minor" to clearer
"base/subunit" currency naming throughout entire codebase, translations,
documentation, tests, and services.

BREAKING CHANGES:

1. **Service API Parameters Renamed**:
   - `get_chartdata`: `minor_currency` → `subunit_currency`
   - `get_apexcharts_yaml`: Updated service_data references from
     `minor_currency: true` to `subunit_currency: true`
   - All automations/scripts using these parameters MUST be updated

2. **Configuration Option Key Changed**:
   - Config entry option: Display mode setting now uses new terminology
   - Internal key: `currency_display_mode` values remain "base"/"subunit"
   - User-facing labels updated in all 5 languages (de, en, nb, nl, sv)

3. **Sensor Entity Key Renamed**:
   - `current_interval_price_major` → `current_interval_price_base`
   - Entity ID changes: `sensor.tibber_home_current_interval_price_major`
     → `sensor.tibber_home_current_interval_price_base`
   - Energy Dashboard configurations MUST update entity references

4. **Function Signatures Changed**:
   - `format_price_unit_major()` → `format_price_unit_base()`
   - `format_price_unit_minor()` → `format_price_unit_subunit()`
   - `get_price_value()`: Parameter `in_euro` deprecated in favor of
     `config_entry` (backward compatible for now)

5. **Translation Keys Renamed**:
   - All language files: Sensor translation key
     `current_interval_price_major` → `current_interval_price_base`
   - Service parameter descriptions updated in all languages
   - Selector options updated: Display mode dropdown values

Changes by Category:

**Core Code (Python)**:
- const.py: Renamed all format_price_unit_*() functions, updated docstrings
- entity_utils/helpers.py: Updated get_price_value() with config-driven
  conversion and backward-compatible in_euro parameter
- sensor/__init__.py: Added display mode filtering for base currency sensor
- sensor/core.py:
  * Implemented suggested_display_precision property for dynamic decimal places
  * Updated native_unit_of_measurement to use get_display_unit_string()
  * Updated all price conversion calls to use config_entry parameter
- sensor/definitions.py: Renamed entity key and updated all
  suggested_display_precision values (2 decimals for most sensors)
- sensor/calculators/*.py: Updated all price conversion calls (8 calculators)
- sensor/helpers.py: Updated aggregate_price_data() signature with config_entry
- sensor/attributes/future.py: Updated future price attributes conversion

**Services**:
- services/chartdata.py: Renamed parameter minor_currency → subunit_currency
  throughout (53 occurrences), updated metadata calculation
- services/apexcharts.py: Updated service_data references in generated YAML
- services/formatters.py: Renamed parameter use_minor_currency →
  use_subunit_currency in aggregate_hourly_exact() and get_period_data()
- sensor/chart_metadata.py: Updated default parameter name

**Translations (5 Languages)**:
- All /translations/*.json:
  * Added new config step "display_settings" with comprehensive explanations
  * Renamed current_interval_price_major → current_interval_price_base
  * Updated service parameter descriptions (subunit_currency)
  * Added selector.currency_display_mode.options with translated labels
- All /custom_translations/*.json:
  * Renamed sensor description keys
  * Updated chart_metadata usage_tips references

**Documentation**:
- docs/user/docs/actions.md: Updated parameter table and feature list
- docs/user/versioned_docs/version-v0.21.0/actions.md: Backported changes

**Tests**:
- Updated 7 test files with renamed parameters and conversion logic:
  * test_connect_segments.py: Renamed minor/major to subunit/base
  * test_period_data_format.py: Updated period price conversion tests
  * test_avg_none_fallback.py: Fixed tuple unpacking for new return format
  * test_best_price_e2e.py: Added config_entry parameter to all calls
  * test_cache_validity.py: Fixed cache data structure (price_info key)
  * test_coordinator_shutdown.py: Added repair_manager mock
  * test_midnight_turnover.py: Added config_entry parameter
  * test_peak_price_e2e.py: Added config_entry parameter, fixed price_avg → price_mean
  * test_percentage_calculations.py: Added config_entry mock

**Coordinator/Period Calculation**:
- coordinator/periods.py: Added config_entry parameter to
  calculate_periods_with_relaxation() calls (2 locations)

Migration Guide:

1. **Update Service Calls in Automations/Scripts**:
   \`\`\`yaml
   # Before:
   service: tibber_prices.get_chartdata
   data:
     minor_currency: true

   # After:
   service: tibber_prices.get_chartdata
   data:
     subunit_currency: true
   \`\`\`

2. **Update Energy Dashboard Configuration**:
   - Settings → Dashboards → Energy
   - Replace sensor entity:
     `sensor.tibber_home_current_interval_price_major` →
     `sensor.tibber_home_current_interval_price_base`

3. **Review Integration Configuration**:
   - Settings → Devices & Services → Tibber Prices → Configure
   - New "Currency Display Settings" step added
   - Default mode depends on currency (EUR → subunit, Scandinavian → base)

Rationale:

The "major/minor" terminology was confusing and didn't clearly communicate:
- **Major** → Unclear if this means "primary" or "large value"
- **Minor** → Easily confused with "less important" rather than "smaller unit"

New terminology is precise and self-explanatory:
- **Base currency** → Standard ISO currency (€, kr, $, £)
- **Subunit currency** → Fractional unit (ct, øre, ¢, p)

This aligns with:
- International terminology (ISO 4217 standard)
- Banking/financial industry conventions
- User expectations from payment processing systems

Impact: Aligns currency terminology with international standards. Users must
update service calls, automations, and Energy Dashboard configuration after
upgrade.

Refs: User feedback session (December 2025) identified terminology confusion
2025-12-11 08:26:30 +00:00
Julian Pawlowski
284a7f4291 fix(periods): Periods are now correctly recalculated after tomorrow prices became available. 2025-12-09 16:57:57 +00:00
Julian Pawlowski
51a99980df feat(sensors)!: add configurable median/mean display for average sensors
Add user-configurable option to choose between median and arithmetic mean
as the displayed value for all 14 average price sensors, with the alternate
value exposed as attribute.

BREAKING CHANGE: Average sensor default changed from arithmetic mean to
median. Users who rely on arithmetic mean behavior may use the price_mean attribue now, or must manually reconfigure
via Settings → Devices & Services → Tibber Prices → Configure → General
Settings → "Average Sensor Display" → Select "Arithmetic Mean" to get this as sensor state.

Affected sensors (14 total):
- Daily averages: average_price_today, average_price_tomorrow
- 24h windows: trailing_price_average, leading_price_average
- Rolling hour: current_hour_average_price, next_hour_average_price
- Future forecasts: next_avg_3h, next_avg_6h, next_avg_9h, next_avg_12h

Implementation:
- All average calculators now return (mean, median) tuples
- User preference controls which value appears in sensor state
- Alternate value automatically added to attributes
- Period statistics (best_price/peak_price) extended with both values

Technical changes:
- New config option: CONF_AVERAGE_SENSOR_DISPLAY (default: "median")
- Calculator functions return tuples: (avg, median)
- Attribute builders: add_alternate_average_attribute() helper function
- Period statistics: price_avg → price_mean + price_median
- Translations: Updated all 5 languages (de, en, nb, nl, sv)
- Documentation: AGENTS.md, period-calculation.md, recorder-optimization.md

Migration path:
Users can switch back to arithmetic mean via:
Settings → Integrations → Tibber Prices → Configure
→ General Settings → "Average Sensor Display" → "Arithmetic Mean"

Impact: Median is more resistant to price spikes, providing more stable
automation triggers. Statistical analysis from coordinator still uses
arithmetic mean (e.g., trailing_avg_24h for rating calculations).

Co-developed-with: GitHub Copilot <copilot@github.com>
2025-12-08 17:53:40 +00:00
Julian Pawlowski
98512672ae feat(lifecycle): implement HA entity best practices for state management
Implemented comprehensive entity lifecycle patterns following Home Assistant
best practices for proper state management and history tracking.
Changes:
- entity.py: Added available property to base class
  - Returns False when coordinator has no data or last_update_success=False
  - Prevents entities from showing stale data during errors
  - Auth failures trigger reauth flow via ConfigEntryAuthFailed

- sensor/core.py: Added state restore and background task handling
  - Changed inheritance: SensorEntity → RestoreSensor
  - Restore native_value from SensorExtraStoredData in async_added_to_hass()
  - Chart sensors restore response data from attributes
  - Converted blocking service calls to background tasks using hass.async_create_task()
  - Eliminates 194ms setup warning by making async_added_to_hass non-blocking

- binary_sensor/core.py: Added state restore and force_update
  - Changed inheritance: BinarySensorEntity → RestoreEntity + BinarySensorEntity
  - Restore is_on state in async_added_to_hass()
  - Added available property override for connection sensor (always True)
  - Added force_update property for connection sensor to track all state changes
  - Other binary sensors use base available logic

- AGENTS.md: Documented entity lifecycle patterns in Common Pitfalls
  - Added "Entity Lifecycle & State Management" section
  - Documents available, state restore, and force_update patterns
  - Explains why each pattern matters for proper HA integration

Impact: Entities no longer show stale data during errors, history has no gaps
after HA restart, connection state changes are properly tracked, and config
entry setup completes in <200ms (under HA threshold).

All patterns verified against HA developer documentation:
https://developers.home-assistant.io/docs/core/entity/
2025-12-07 17:24:41 +00:00
Julian Pawlowski
763a6b76b9 perf(entities): exclude non-essential attributes from recorder history
Implement _unrecorded_attributes in both sensor and binary_sensor
entities to prevent Home Assistant Recorder database bloat.

Excluded attributes (60-85% size reduction per state):
- Descriptions/help text (static, large strings)
- Large nested structures (periods, trend_attributes, chart data)
- Frequently changing diagnostics (icon_color, cache_age)
- Static/rarely changing config (currency, resolution)
- Temporary/time-bound data (next_api_poll, last_*)
- Redundant/derived data (price_spread, diff_%)

Kept for history analysis:
- timestamp (always first), all price values
- Period timing (start, end, duration_minutes)
- Price statistics (avg, min, max)
- Boolean status flags, relaxation_active

Impact: Reduces attribute size from ~3-8 KB to ~0.5-1.5 KB per state
change. Expected savings: ~1 GB per month for typical installation.

See: https://developers.home-assistant.io/docs/core/entity/#excluding-state-attributes-from-recorder-history
2025-12-07 16:57:40 +00:00
Julian Pawlowski
f92fc3b444 refactor(services): remove gradient_stop, use fixed 50% gradient
Implementation flaw discovered: gradient_stop calculated as
`(avg - min) / (max - min)` for combined data produces one value
applied to ALL series. Each series (VERY_CHEAP, NORMAL, VERY_EXPENSIVE)
has different min/max ranges, so the same gradient stop position
represents a different absolute price in each series.

Example failure case:
- VERY_CHEAP: 10-20 ct → 50% at 15 ct (below overall avg!)
- VERY_EXPENSIVE: 40-50 ct → 50% at 45 ct (above overall avg!)

Conclusion: Gradient shows middle of each series range, not average
price position.

Solution: Fixed 50% gradient purely for visual appeal. Semantic
information provided by:
- Series colors (CHEAP/NORMAL/EXPENSIVE)
- Grid lines (implicitly show average)
- Dynamic Y-axis bounds (optimal scaling via chart_metadata sensor)

Changes:
- sensor/chart_metadata.py: Remove gradient_stop extraction
- services/get_apexcharts_yaml.py: Fixed gradient at [50, 100]
- custom_translations/*.json: Remove gradient_stop references

Impact: Honest visualization with no false semantic signals. Feature
was never released, clean removal without migration.
2025-12-05 20:51:30 +00:00
Julian Pawlowski
6922e52368 feat(sensors): add chart_metadata sensor for lightweight chart configuration
Implemented new chart_metadata diagnostic sensor that provides essential
chart configuration values (yaxis_min, yaxis_max, gradient_stop) as
attributes, enabling dynamic chart configuration without requiring
async service calls in templates.

Sensor implementation:
- New chart_metadata.py module with metadata-only service calls
- Automatically calls get_chartdata with metadata="only" parameter
- Refreshes on coordinator updates (new price data or user data)
- State values: "pending", "ready", "error"
- Enabled by default (critical for chart features)

ApexCharts YAML generator integration:
- Checks for chart_metadata sensor availability before generation
- Uses template variables to read sensor attributes dynamically
- Fallback to fixed values (gradient_stop=50%) if sensor unavailable
- Creates separate notifications for two independent issues:
  1. Chart metadata sensor disabled (reduced functionality warning)
  2. Required custom cards missing (YAML won't work warning)
- Both notifications explain YAML generation context and provide
  complete fix instructions with regeneration requirement

Configuration:
- Supports configuration.yaml: tibber_prices.chart_metadata_config
- Optional parameters: day, minor_currency, resolution
- Defaults to minor_currency=True for ApexCharts compatibility

Translation additions:
- Entity name and state translations (all 5 languages)
- Notification messages for sensor unavailable and missing cards
- best_price_period_name for tooltip formatter

Binary sensor improvements:
- tomorrow_data_available now enabled by default (critical for automations)
- data_lifecycle_status now enabled by default (critical for debugging)

Impact: Users get dynamic chart configuration with optimized Y-axis scaling
and gradient positioning without manual calculations. ApexCharts YAML
generation now provides clear, actionable notifications when issues occur,
ensuring users understand why functionality is limited and how to fix it.
2025-12-05 20:30:54 +00:00
Julian Pawlowski
6b78cd757f refactor: simplify needs_tomorrow_data() - remove tomorrow_date parameter
Changed needs_tomorrow_data() to auto-calculate tomorrow date using
get_intervals_for_day_offsets([1]) helper instead of requiring explicit
tomorrow_date parameter.

Changes:
- coordinator/helpers.py: needs_tomorrow_data() signature simplified
  * Uses get_intervals_for_day_offsets([1]) to detect tomorrow intervals
  * No longer requires tomorrow_date parameter (calculated automatically)
  * Consistent with all other data access patterns

- coordinator/data_fetching.py: Removed tomorrow_date calculation and passing
  * Removed unused date import
  * Simplified method call: needs_tomorrow_data() instead of needs_tomorrow_data(tomorrow_date)

- sensor/calculators/lifecycle.py: Updated calls to _needs_tomorrow_data()
  * Removed tomorrow_date variable where it was only used for this call
  * Combined nested if statements with 'and' operator

Impact: Cleaner API, fewer parameters to track, consistent with other
helper functions that auto-calculate dates based on current time.
2025-11-24 16:26:08 +00:00
Julian Pawlowski
2de793cfda refactor: migrate from multi-home to single-home-per-coordinator architecture
Changed from centralized main+subentry coordinator pattern to independent
coordinators per home. Each config entry now manages its own home data
with its own API client and access token.

Architecture changes:
- API Client: async_get_price_info() changed from home_ids: set[str] to home_id: str
  * Removed GraphQL alias pattern (home0, home1, ...)
  * Single-home query structure without aliasing
  * Simplified response parsing (viewer.home instead of viewer.home0)

- Coordinator: Removed main/subentry distinction
  * Deleted is_main_entry() and _has_existing_main_coordinator()
  * Each coordinator fetches its own data independently
  * Removed _find_main_coordinator() and _get_configured_home_ids()
  * Simplified _async_update_data() - no subentry logic
  * Added _home_id instance variable from config_entry.data

- __init__.py: New _get_access_token() helper
  * Handles token retrieval for both parent and subentries
  * Subentries find parent entry to get shared access token
  * Creates single API client instance per coordinator

- Data structures: Flat single-home format
  * Old: {"homes": {home_id: {"price_info": [...]}}}
  * New: {"home_id": str, "price_info": [...], "currency": str}
  * Attribute name: "periods" → "pricePeriods" (consistent with priceInfo)

- helpers.py: Removed get_configured_home_ids() (no longer needed)
  * parse_all_timestamps() updated for single-home structure

Impact: Each home operates independently with its own lifecycle tracking,
caching, and period calculations. Simpler architecture, easier debugging,
better isolation between homes.
2025-11-24 16:24:37 +00:00
Julian Pawlowski
981fb08a69 refactor(price_info): price data handling to use unified interval retrieval
- Introduced `get_intervals_for_day_offsets` helper to streamline access to price intervals for yesterday, today, and tomorrow.
- Updated various components to replace direct access to `priceInfo` with the new helper, ensuring a flat structure for price intervals.
- Adjusted calculations and data processing methods to accommodate the new data structure.
- Enhanced documentation to reflect changes in caching strategy and data structure.
2025-11-24 10:49:34 +00:00
Julian Pawlowski
ea21b229ee refactor(calculators): consolidate duplicate data access patterns
Refactored Trend, Timing, and Lifecycle calculators to use BaseCalculator
helper methods instead of duplicating data access logic.

Changes:
- TrendCalculator: Simplified 12 lines of repeated price_info access to
  3-4 clean property calls (intervals_today/tomorrow, get_all_intervals)
- TimingCalculator: Replaced direct coordinator.data checks with has_data()
- LifecycleCalculator: Replaced 5 lines of nested gets with 2 helper calls

Benefits:
- Eliminated 10+ duplicate data access patterns
- Consistent None-handling across all calculators
- Single source of truth for coordinator data access
- Easier to maintain (changes propagate automatically)

BaseCalculator helpers used:
- has_data(): Replaces 'if not self.coordinator.data:' checks
- intervals_today/tomorrow: Direct property access to day intervals
- get_intervals(day): Safe day-specific interval retrieval
- get_all_intervals(): Combined yesterday+today+tomorrow
- coordinator_data: Property for coordinator.data access

Validation:
- Type checker: 0 errors, 0 warnings
- Tests: 347 passed, 2 skipped (no behavior change)
- Net: 19 deletions, 14 insertions (5 lines removed, patterns simplified)

Impact: Cleaner code, reduced duplication, consistent error handling.
Future calculator additions will automatically benefit from centralized
data access patterns.
2025-11-22 14:54:06 +00:00