Commit graph

13 commits

Author SHA1 Message Date
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
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
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
11d4cbfd09 feat(config_flow): add price level gap tolerance for Tibber API level field
Implement gap tolerance smoothing for Tibber's price level classification
(VERY_CHEAP/CHEAP/NORMAL/EXPENSIVE/VERY_EXPENSIVE), separate from the existing
rating_level gap tolerance (LOW/NORMAL/HIGH).

New feature:
- Add CONF_PRICE_LEVEL_GAP_TOLERANCE config option with separate UI step
- Implement _apply_level_gap_tolerance() using same bidirectional gravitational
  pull algorithm as rating gap tolerance
- Add _build_level_blocks() and _merge_small_level_blocks() helper functions

Config flow changes:
- Add new "price_level" options step with dedicated schema
- Add menu entry "🏷️ Preisniveau" / "🏷️ Price Level"
- Include translations for all 5 languages (de, en, nb, nl, sv)

Bug fixes:
- Use copy.deepcopy() for price intervals before enrichment to prevent
  in-place modification of cached raw API data, which caused gap tolerance
  changes to not take effect when reverting settings
- Clear transformation cache in invalidate_config_cache() to ensure
  re-enrichment with new settings

Logging improvements:
- Reduce options update handler from 4 INFO messages to 1 DEBUG message
- Move level_filtering and period_overlap debug logs to .details logger
  for granular control via configuration.yaml

Technical details:
- level_gap_tolerance is tracked separately in transformation config hash
- Algorithm: Identifies small blocks (≤ tolerance) and merges them into
  the larger neighboring block using gravitational pull calculation
- Default: 1 (smooth single isolated intervals), Range: 0-4

Impact: Users can now stabilize Tibber's price level classification
independently from the internal rating_level calculation. Prevents
automation flickering caused by brief price level changes in Tibber's API.
2025-12-22 20:25:30 +00:00
Julian Pawlowski
64cf842719 fix(rating): improve gap tolerance to find dominant large blocks
The gap tolerance algorithm now looks through small intermediate blocks
to find the first LARGE block (> gap_tolerance) in each direction.
This ensures small isolated rating intervals are merged into the
correct dominant block, not just the nearest neighbor.

Example: NORMAL(large) HIGH(1) NORMAL(1) HIGH(large)
Before: HIGH at 05:45 merged into NORMAL (wrong - nearest neighbor)
After:  NORMAL at 06:00 merged into HIGH (correct - dominant block)

Also collects all merge decisions BEFORE applying them, preventing
order-dependent outcomes when multiple small blocks are adjacent.

Impact: Rating transitions now appear at visually logical positions
where prices actually change direction, not at arbitrary boundaries.
2025-12-22 13:28:25 +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
e04e38d09f refactor(logging): remove verbose debug logging from price enrichment
Removed excessive debug logging in enrich_price_info_with_differences():
- Deleted per-interval "Processing" messages (cluttered logs)
- Kept boundary INFO messages (enrichment start/skip counts)
- Removed unused variable expected_intervals_24h
- Removed unused parameter day_label from _process_price_interval()

Impact: Cleaner logs, no functional changes. Reduces log volume during
price data processing.
2025-11-25 20:44:39 +00:00
Julian Pawlowski
7e47ef5995 docs: fix attribute names in AGENTS.md examples
Updated attribute ordering documentation to use correct names:
- "periods" → "pricePeriods" (matches code since refactoring)
- "intervals" → "priceInfo" (flat list structure)

Impact: Documentation now matches actual code structure.
2025-11-24 16:26:23 +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
9c3c094305 fix(calculations): handle negative electricity prices correctly
Fixed multiple calculation issues with negative prices (Norway/Germany
renewable surplus scenarios):

Bug #6: Rating threshold validation with dead code
- Added threshold validation (low >= high) with warning
- Returns NORMAL as fallback for misconfigured thresholds

Bug #7: Min/Max functions returning 0.0 instead of None
- Changed default from 0.0 to None when window is empty
- Prevents misinterpretation (0.0 looks like price with negatives)

Bug #9: Period price diff percentage wrong sign with negative reference
- Use abs(ref_price) in percentage calculation
- Correct percentage direction for negative prices

Bug #10: Trend diff percentage wrong sign with negative current price
- Use abs(current_interval_price) in percentage calculation
- Correct trend direction when prices cross zero

Bug #11: later_half_diff calculation failed for negative prices
- Changed condition from `if current_interval_price > 0` to `!= 0`
- Use abs(current_interval_price) for percentage

Changes:
- utils/price.py: Add threshold validation, use abs() in percentages
- utils/average.py: Return None instead of 0.0 for empty windows
- period_statistics.py: Use abs() for reference prices
- trend.py: Use abs() for current prices, fix zero-check condition
- tests: 95+ new tests covering negative/zero/mixed price scenarios

Impact: All calculations work correctly with negative electricity prices.
Percentages show correct direction regardless of sign.
2025-11-22 04:45:23 +00:00
Julian Pawlowski
c2b9908e69 refactor(naming): complete class naming convention alignment
Renamed 25 public classes + 1 Enum to include TibberPrices prefix
following Home Assistant integration naming standards.

All classes now follow pattern: TibberPrices{SemanticPurpose}
No package hierarchy in names (import path is namespace).

Key changes:
- Coordinator module: DataFetcher, DataTransformer, ListenerManager,
  PeriodCalculator, TimeService (203 usages), CacheData
- Config flow: CannotConnectError, InvalidAuthError
- Entity utils: IconContext
- Sensor calculators: BaseCalculator + 8 subclasses
- Period handlers: 5 NamedTuples (PeriodConfig, PeriodData,
  PeriodStatistics, ThresholdConfig, IntervalCriteria)
- Period handlers: SpikeCandidateContext (dataclass → NamedTuple)
- API: QueryType Enum

Documentation updates:
- AGENTS.md: Added Pyright code generation guidelines
- planning/class-naming-refactoring-plan.md: Complete execution log

Quality metrics:
- 0 Pyright errors (strict type checking)
- 0 Ruff errors (linting + formatting)
- All hassfest checks passed
- 79 files validated

Impact: Aligns with HA Core standards (TibberDataCoordinator pattern).
No user-facing changes - internal refactor only.
2025-11-20 11:22:53 +00:00
Julian Pawlowski
625bc222ca refactor(coordinator): centralize time operations through TimeService
Introduce TimeService as single source of truth for all datetime operations,
replacing direct dt_util calls throughout the codebase. This establishes
consistent time context across update cycles and enables future time-travel
testing capability.

Core changes:
- NEW: coordinator/time_service.py with timezone-aware datetime API
- Coordinator now creates TimeService per update cycle, passes to calculators
- Timer callbacks (#2, #3) inject TimeService into entity update flow
- All sensor calculators receive TimeService via coordinator reference
- Attribute builders accept time parameter for timestamp calculations

Key patterns replaced:
- dt_util.now() → time.now() (single reference time per cycle)
- dt_util.parse_datetime() + as_local() → time.get_interval_time()
- Manual interval arithmetic → time.get_interval_offset_time()
- Manual day boundaries → time.get_day_boundaries()
- round_to_nearest_quarter_hour() → time.round_to_nearest_quarter()

Import cleanup:
- Removed dt_util imports from ~30 files (calculators, attributes, utils)
- Restricted dt_util to 3 modules: time_service.py (operations), api/client.py
  (rate limiting), entity_utils/icons.py (cosmetic updates)
- datetime/timedelta only for TYPE_CHECKING (type hints) or duration arithmetic

Interval resolution abstraction:
- Removed hardcoded MINUTES_PER_INTERVAL constant from 10+ files
- New methods: time.minutes_to_intervals(), time.get_interval_duration()
- Supports future 60-minute resolution (legacy data) via TimeService config

Timezone correctness:
- API timestamps (startsAt) already localized by data transformation
- TimeService operations preserve HA user timezone throughout
- DST transitions handled via get_expected_intervals_for_day() (future use)

Timestamp ordering preserved:
- Attribute builders generate default timestamp (rounded quarter)
- Sensors override when needed (next interval, daily midnight, etc.)
- Platform ensures timestamp stays FIRST in attribute dict

Timer integration:
- Timer #2 (quarter-hour): Creates TimeService, calls _handle_time_sensitive_update(time)
- Timer #3 (30-second): Creates TimeService, calls _handle_minute_update(time)
- Consistent time reference for all entities in same update batch

Time-travel readiness:
- TimeService.with_reference_time() enables time injection (not yet used)
- All calculations use time.now() → easy to simulate past/future states
- Foundation for debugging period calculations with historical data

Impact: Eliminates timestamp drift within update cycles (previously 60+ independent
dt_util.now() calls could differ by milliseconds). Establishes architecture for
time-based testing and debugging features.
2025-11-19 18:36:12 +00:00
Julian Pawlowski
d52eb6b788 refactor(utils): create utils package and consolidate constants
Reorganized utility modules into structured package:
- average_utils.py → utils/average.py
- price_utils.py → utils/price.py
- Created utils/__init__.py with clean exports

Moved MINUTES_PER_INTERVAL to const.py (centralized constant
management), with re-exports in utils modules for backward
compatibility during migration.

Added comprehensive package docstring explaining scope:
- Pure data transformation functions (stateless)
- No HA entity/coordinator dependencies
- Clear separation from entity_utils/ (entity-specific logic)

Impact: Cleaner module structure, easier navigation. Follows
file organization policy from AGENTS.md (keep root clean).
2025-11-18 20:06:46 +00:00
Renamed from custom_components/tibber_prices/price_utils.py (Browse further)