Commit graph

60 commits

Author SHA1 Message Date
Julian Pawlowski
e01cc5d447 feat(services): allow entity IDs as service parameter values
Add entity_resolver module that lets all service parameters accept
HA entity references in place of literal values. The entity's current
state (or a specific attribute via the @attr syntax) is resolved at
call time and coerced to the expected Python type.

Syntax:
  "sensor.washing_duration"           → uses entity state
  "sensor.washing_duration@run_minutes" → uses entity attribute

Apply or_entity_ref() and resolve_entity_references() to all five
service handlers (get_price, find_cheapest_block, find_cheapest_hours,
find_cheapest_schedule, get_chartdata) for every parameter where a
dynamic value from another entity is useful (duration, start/end times,
offsets, etc.).

Add five new translation keys for entity-resolution error messages
(invalid_entity_reference, entity_not_found, entity_attribute_not_found,
entity_state_unavailable, entity_value_conversion_failed) across all
five language files.

Fix pytest warning filter to suppress AsyncMock cleanup noise, and
update test_resource_cleanup to mock hass.config_entries.async_entries
so the blueprint-removal path in async_remove_entry does not raise.

Impact: Automations and scripts can pass sensor entity IDs as service
parameters (e.g. duration from a sensor) instead of having to use
template-based workarounds.
2026-04-20 18:44:24 +00:00
Julian Pawlowski
31fca73ccd feat(services): add sequential parameter to find_cheapest_schedule
When sequential: true, tasks are placed in declaration order instead of
being sorted by duration. Each task's search window starts after the
previous task ends (plus gap_minutes). If a task cannot be placed, all
subsequent tasks in the chain are also marked unscheduled.

Adds 12 tests covering ordering, chaining, gap enforcement, and
chain-breaking behavior.

Impact: Users can now schedule dependent appliances (e.g., washing
machine → dryer) in a single find_cheapest_schedule call with guaranteed
order, instead of chaining two find_cheapest_block calls.
2026-04-19 14:17:32 +00:00
Julian Pawlowski
303a7c7835 feat(pricing): add relaxation logic for progressive filter loosening
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
Implement a new service that progressively relaxes user-defined filters to ensure a result is always returned when price data is available. This includes three phases: halving the minimum distance from average, expanding level filters, and reducing duration.

Impact: Users will receive results even when strict filters would otherwise yield no matches, improving the reliability of scheduling actions.

feat(pricing): enhance scheduling actions with new parameters

Introduce new parameters `smooth_outliers`, `min_distance_from_avg`, and `allow_relaxation` to scheduling actions, allowing for better control over price selection and ensuring results are meaningfully different from average prices.

Impact: Users can now fine-tune their scheduling actions to avoid marginal savings and ensure more uniform pricing within selected windows.

docs(scheduling): update documentation for new features

Revise the scheduling actions documentation to include new parameters and their effects, such as outlier smoothing and minimum distance from average, along with examples for better user understanding.

Impact: Users will have clearer guidance on how to utilize new features effectively in their automations.

test(scheduling): add tests for new relaxation logic

Implement unit tests to verify the behavior of the new relaxation logic in scheduling actions, ensuring that filters are correctly relaxed and results are returned as expected.

Impact: Increased test coverage and reliability of the scheduling features.
2026-04-18 21:27:05 +00:00
Julian Pawlowski
2b63440933 refactor(periods): enhance peak period filtering and validation logic
Some checks failed
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
Auto-Tag on Version Bump / Check and create version tag (push) Has been cancelled
Improve the filtering of peak periods to eliminate cross-day artifacts and ensure that only genuine high-price windows are retained. This includes adjustments to the criteria for peak classification and the introduction of validation against previous day's prices for overnight intervals.

Impact: Users will experience more accurate peak pricing data, reducing misleading peak classifications on flat days.
2026-04-17 22:24:18 +00:00
Julian Pawlowski
ebcb9cfe77 fix(interval_pool): rebuild index after dead interval cleanup without empty groups
Cherry-pick of v0.30.1 hotfix (ec3bc9f). When _cleanup_dead_intervals
compacted group lists but no groups became fully empty, the index retained
stale interval_index values pointing past compacted list ends, causing
IndexError in _get_cached_intervals.

Now rebuilds the index whenever dead intervals are removed, even if no
groups are deleted.

Includes regression test for Issue #118 and updated touch operation tests
to reflect that GC now runs immediately after touch-only paths.

Closes #118

Impact: Eliminates IndexError crash for users with brand-new Tibber accounts
that have limited price history, where partial group compaction was most likely.
2026-04-17 12:00:37 +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
c89248d493 feat(services): add reason codes and schedule comparison details to find services
Add structured reason codes to no-result responses for find_cheapest_block,
find_cheapest_hours, and find_cheapest_schedule. Each handler now classifies
why no result was returned: no_data_in_range, no_intervals_matching_level_filter,
insufficient_intervals_after_filter, or insufficient_contiguous_window.

Add include_comparison_details flag to find_cheapest_schedule. When enabled,
each scheduled task includes a price_comparison field showing the most expensive
alternative window (mean, min, max, start, end) for cost-savings context.

Document stable reason code contracts in en.json service descriptions.
Add corresponding field translations to all locales (de, nb, nl, sv).

Impact: Automations and scripts can now react to why no window was found,
and schedules can display concrete savings vs. worst-case pricing.
2026-04-12 12:47:11 +00:00
Julian Pawlowski
6e0613c055 feat(services): add 5 scheduling services for price-optimized time windows
New services for finding optimal electricity price windows:
- find_cheapest_block: Cheapest contiguous time block (e.g., dishwasher)
- find_cheapest_hours: Cheapest N hours, non-contiguous (e.g., EV charging)
- find_cheapest_schedule: Multi-task scheduling with no-overlap (e.g., shared circuit)
- find_most_expensive_block: Most expensive contiguous block (peak avoidance)
- find_most_expensive_hours: Most expensive N hours (consumption shifting)

Key features:
- Flexible search range (today, tomorrow, today+tomorrow, rolling window)
- Power profile support for variable consumption patterns
- Price level filtering (e.g., only CHEAP/VERY_CHEAP intervals)
- Comparison details showing savings vs. alternatives
- Sliding window algorithm (O(n)) for block search, greedy scheduling
  for multi-task optimization

Also includes:
- Shared validation utilities (search range, price level, power profile)
- entry_id now optional on all services (auto-selects single home)
- Input validation for existing services (time range, filter conflicts)
- Service icons for all new and existing services
- Translations for all 5 languages (en, de, nb, nl, sv)
- Removed 10 unused config.error translation keys (replaced by exceptions)
- Tests for price window algorithms and search range resolution

Impact: Users can find optimal time windows for appliances, EV charging,
and multi-device scheduling via HA service calls. Existing services
improved with optional entry_id and better input validation.
2026-04-11 18:58:27 +00:00
Julian Pawlowski
c610dbe1a3 fix(tests): move LogCaptureFixture import under TYPE_CHECKING for better type hinting 2026-04-11 11:05:55 +00:00
Julian Pawlowski
124824a2ea test(sensors): update timer assignment tests for renamed/new trend sensors
test_trend_sensors_use_quarter_hour_timer():
  - Replaced price_trend_Xh keys with price_outlook_Xh
  - Added 7 price_trajectory_Xh keys to the assertion list
  - Updated docstring from "price trend" to "price outlook/trajectory"

Impact: Test suite passes with renamed and new sensor keys.
2026-04-09 16:09:14 +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
1e1c8d5299 feat(periods): handle flat days and absolute low-price scenarios
Three complementary fixes for pathological price days:

1. Adaptive min_periods for flat days (CV ≤ 10%):
   On days with nearly uniform prices (e.g. solar surplus), enforcing
   multiple distinct cheap periods is geometrically impossible.
   _compute_day_effective_min() detects CV ≤ LOW_CV_FLAT_DAY_THRESHOLD
   and reduces the effective target to 1 for that day (best price only;
   peak price always runs full relaxation).

2. min_distance scaling on absolute low-price days:
   When the daily average drops below 0.10 EUR (10 ct), percentage-based
   min_distance becomes unreliable. The threshold is scaled linearly to
   zero so the filter neither accepts the entire day nor blocks everything.

3. CV quality gate bypass for absolute low-price periods:
   Periods with a mean below 0.10 EUR may show high relative CV even
   though the absolute price differences are fractions of a cent.
   Both _check_period_quality() and _check_merge_quality_gate() now
   bypass the CV gate below this threshold.

Additionally: span-aware flex warnings now emit INFO/WARNING when
base_flex >= 25%/30% and at least one "normal" (non-V-shape) day
exists (FLEX_WARNING_VSHAPE_RATIO = 0.5). Previously the constants
were defined but never used.

Updated 3 test assertions in test_best_price_e2e.py: the flat-day
fixture (CV ~5.4%) correctly produces 1 period, not 2.

Impact: Best Price periods now appear reliably on V-shape solar days
and flat-price days. No more "0 periods" on days where the single
cheapest window is a valid and useful result.
2026-04-06 12:18:40 +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
f88d6738e6 fix(validation): enhance user data validation to require active subscription and price info.
Fixes #73
2026-01-20 12:33:45 +00:00
Julian Pawlowski
4b32568665 fix(tests): include current_interval_price_base in interval sensors and remove from known exceptions 2026-01-20 12:06:10 +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
cfc7cf6abc refactor(coordinator): replace DataFetcher with PriceDataManager
Rename and refactor data_fetching.py → price_data_manager.py to reflect
actual responsibilities:
- User data: Fetches directly via API, validates, caches
- Price data: Delegates to IntervalPool (single source of truth)

Key changes:
- Add should_fetch_tomorrow_data() for intelligent API call decisions
- Add include_tomorrow parameter to prevent API spam before 13:00
- Remove cached_price_data property (Pool is source of truth)
- Update tests to use new class name

Impact: Clearer separation of concerns, reduced API calls through
intelligent tomorrow data fetching logic.
2025-12-23 14:13:43 +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
94615dc6cd refactor(interval_pool): improve reliability and test coverage
Added async_shutdown() method for proper cleanup on unload - cancels
debounce and background tasks to prevent orphaned task leaks.

Added Phase 1.5 to GC: removes empty fetch groups after dead interval
cleanup, with index rebuild to maintain consistency.

Added update_batch() to TimestampIndex for efficient batch updates.
Touch operations now use batch updates instead of N remove+add calls.

Rewrote memory leak tests for modular architecture - all 9 tests now
pass using new component APIs (cache, index, gc).

Impact: Prevents task leaks on HA restart/reload, reduces memory
overhead from empty groups, improves touch operation performance.
2025-12-23 10:10:35 +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
aff3350de7 test(sensors): add comprehensive test coverage for mean/median display
Added new test suite and updated existing tests to verify always-both-attributes
behavior.

Changes:
- test_mean_median_display.py: NEW - Tests both attributes always present,
  configurable state display, recorder exclusion, and config changes
- test_avg_none_fallback.py: Updated to test mean/median individually (65 lines)
- test_sensor_timer_assignment.py: Minor updates for compatibility (12 lines)

Coverage: All 399 tests passing, including new edge cases for attribute
presence and recorder integration.
2025-12-18 15:14:22 +00:00
Julian Pawlowski
d00935e697 fix(tests): remove unused mock_config_entry and update price_avg to base currency in percentage calculations 2025-12-13 14:07:16 +00:00
Julian Pawlowski
87f0022baa fix(api): handle None values in API responses to prevent AttributeError
Fixed issue #60 where Tibber API temporarily returning incomplete data
(None values during maintenance) caused AttributeError crashes.

Root cause: `.get(key, default)` returns None when key exists with None value,
causing chained `.get()` calls to crash (None.get() → AttributeError).

Changes:
- api/helpers.py: Use `or {}` pattern in flatten_price_info() to handle
  None values (priceInfo, priceInfoRange, today, tomorrow)
- entity.py: Use `or {}` pattern in _get_fallback_device_info() for address dict
- coordinator/data_fetching.py: Add _validate_user_data() method (67 lines)
  to reject incomplete API responses before caching
- coordinator/data_fetching.py: Modify _get_currency_for_home() to raise
  exceptions instead of silent EUR fallback
- coordinator/data_fetching.py: Add home_id parameter to constructor
- coordinator/core.py: Pass home_id to TibberPricesDataFetcher
- tests/test_user_data_validation.py: Add 12 test cases for validation logic

Architecture improvement: Instead of defensive coding with fallbacks,
implement validation to reject incomplete data upfront. This prevents
caching temporary API errors and ensures currency is always known
(critical for price calculations).

Impact: Integration now handles API maintenance periods gracefully without
crashes. No silent EUR fallbacks - raises exceptions if currency unavailable,
ensuring data integrity. Users see clear errors instead of wrong calculations.

Fixes #60
2025-12-13 14:02:30 +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
6e0310ef7c fix(services): correct period data format for ApexCharts visualization
Period data in array_of_arrays format now generates proper segment structure
for stepline charts. Each period produces 2-3 data points depending on
insert_nulls parameter:

1. Start time with price (begin period)
2. End time with price (hold price level)
3. End time with NULL (terminate segment, only if insert_nulls='segments'/'all')

This enables ApexCharts to correctly display periods as continuous blocks with
clean gaps between them. Previously only start point was generated, causing
periods to render as single points instead of continuous segments.

Changes:
- formatters.py: Updated get_period_data() to generate 2-3 points per period
- formatters.py: Added insert_nulls parameter to control NULL termination
- get_chartdata.py: Pass insert_nulls parameter to get_period_data()
- get_apexcharts_yaml.py: Set insert_nulls='segments' for period overlay
- get_apexcharts_yaml.py: Preserve NULL values in data_generator mapping
- get_apexcharts_yaml.py: Store original price for potential tooltip access
- tests: Added comprehensive period data format tests

Impact: Best price and peak price period overlays now display correctly as
continuous blocks with proper segment separation in ApexCharts cards.
2025-12-03 14:20:46 +00:00
Julian Pawlowski
f70ac9cff6 feat(services): improve ApexCharts segment visualization and fix header display
Simplifies the connect_segments implementation to use a unified bridge-point
approach for all price transitions (up/down/same). Previously used
direction-dependent logic (hold vs connect points) which was unnecessarily
complex.

Changes:
- get_chartdata.py: Bridge points now always use next interval's price at
  boundary timestamp, creating smooth visual connection between segments
- get_chartdata.py: Trailing NULL removal now conditional on insert_nulls mode
  ('segments' removes for header fix, 'all' preserves intentional gaps)
- get_apexcharts_yaml.py: Enable connect_segments by default, activate
  show_states for header min/max display
- get_apexcharts_yaml.py: Remove extrema series (not compatible with
  data_generator approach - ApexCharts requires entity time-series data)
- tests: Move test_connect_segments.py to tests/services/ to mirror source
  structure

Impact: ApexCharts cards now show clean visual connections between price level
segments with proper header statistics display. Trailing NULLs no longer cause
"N/A" in headers for filtered data. Test organization improved for
maintainability.
2025-12-01 11:14:27 +00:00
Copilot
49628f3394
Add connect_segments parameter and fix ApexCharts header N/A display (#46)
* Initial plan

* Add connect_segments parameter to get_chartdata service for visual segment connections

Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>

* Address code review feedback: fix test logic and correct misleading comment

Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>

* Integrate PR45: Remove trailing null values for proper ApexCharts header display

Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>

* Add connect_segments translations for de, nb, nl, sv languages

Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>

* Changes before error encountered

Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>

* Fix hassfest validation: Move time_units from translations to custom_translations

Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>
2025-12-01 03:19:52 +01:00
Julian Pawlowski
a90fef6f2d refactor(scripts): reorganize and standardize development scripts
Major restructuring of the scripts/ directory with consistent output
formatting, improved organization, and stricter error handling.

Breaking Changes:
- Updated development environment to Home Assistant 2025.7+
  - Removed Python 3.12 compatibility (HA 2025.7+ requires Python 3.13)
  - Updated all HA core requirements from 2025.7 requirement files
  - Added new dependencies: python-multipart, uv (for faster package management)
  - Updated GitHub Actions workflows to use Python 3.13

Changes:
- Created centralized output library (scripts/.lib/output.sh)
  - Unified color codes and Unicode symbols
  - Consistent formatting functions (log_header, log_success, log_error, etc.)
  - Support for embedded formatting codes (${BOLD}, ${GREEN}, etc.)

- Reorganized into logical subdirectories:
  - scripts/setup/ - Setup and maintenance scripts
    - bootstrap: Install/update dependencies (used in CI/CD)
    - setup: Full DevContainer setup (pyright, copilot, HACS)
    - reset: Reset config/ directory to fresh state (NEW)
    - sync-hacs: Sync HACS integrations
  - scripts/release/ - Release management scripts
    - prepare: Version bump and tagging
    - suggest-version: Semantic version suggestion
    - generate-notes: Release notes generation
    - check-if-released: Check release status
    - hassfest: Local integration validation

- Updated all scripts with:
  - set -euo pipefail for stricter error handling
  - Consistent SCRIPT_DIR pattern for reliable sourcing
  - Professional output with colors and emojis
  - Unified styling across all 17 scripts

- Removed redundant scripts:
  - scripts/update (was just wrapper around bootstrap)
  - scripts/json_schemas/ (moved to schemas/json/)

- Enhanced clean script:
  - Improved artifact cleanup
  - Better handling of accidental package installations
  - Hints for reset and deep clean options

- New reset script features:
  - Standard mode: Keep configuration.yaml
  - Full mode (--full): Reset configuration.yaml from git
  - Automatic re-setup after reset

- Updated documentation:
  - AGENTS.md: Updated script references and workflow guidance
  - docs/development/: Updated all references to new script structure

Impact: Development environment now requires Python 3.13 and Home Assistant
2025.7+. Developers get consistent, professional script output with better
error handling and logical organization. Single source of truth for styling
makes future updates trivial.
2025-11-26 13:11:52 +00:00
Julian Pawlowski
74789877ff test: fix async mocking and add noqa comments for private access
Fixed test issues:
- test_resource_cleanup.py: Use AsyncMock for async_unload_entry
  (was MagicMock, caused TypeError with async context)
- Added # noqa: SLF001 comments to all private member access in tests
  (18 instances - legitimate test access patterns)

Test files updated:
- test_resource_cleanup.py (AsyncMock fix)
- test_interval_pool_memory_leak.py (8 noqa comments)
- test_interval_pool_optimization.py (4 noqa comments)

Impact: All tests pass linting, async tests execute correctly.
2025-11-25 20:44:39 +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
9ee7f81164 fix(coordinator): invalidate transformation cache when source data changes
Fixes bug where lifecycle sensor attributes (data_completeness, tomorrow_available)
didn't update after tomorrow data was successfully fetched from API.

Root cause: DataTransformer had cached transformation data but no mechanism to detect
when source API data changed (only checked config and midnight turnover).

Changes:
- coordinator/data_transformation.py: Track source_data_timestamp and invalidate cache
  when timestamp changes (detects new API data arrival)
- coordinator/data_transformation.py: Integrate period calculation into DataTransformer
  (calculate_periods_fn parameter) for complete single-layer caching
- coordinator/core.py: Remove duplicate transformation cache (_cached_transformed_data,
  _last_transformation_config), simplify _transform_data_for_*() to direct delegation
- tests/test_tomorrow_data_refresh.py: Add 3 regression tests for cache invalidation
  (new timestamp, config change behavior, cache preservation)

Impact: Lifecycle sensor attributes now update correctly when new API data arrives.
Reduced code by ~40 lines in coordinator, consolidated caching to single layer.
All 350 tests passing.
2025-11-23 13:10:19 +00:00
Julian Pawlowski
32857c0cc0 test: remove obsolete lifecycle callback tests
Removed tests for the lifecycle callback system that was removed in
commit 48d6e25.

Also fixed commit f373c01 which incorrectly added test_lifecycle_tomorrow_update.py
instead of deleting it - this commit properly removes it.

Changes:
- tests/test_chart_data_push_updates.py: Deleted (235 lines)
- tests/test_lifecycle_tomorrow_update.py: Deleted (174 lines)
- tests/test_resource_cleanup.py: Removed lifecycle callback test method

Impact: Test suite now has 343 tests (down from 349). All tests pass.
No functionality affected - only test cleanup.
2025-11-22 13:04:47 +00:00
Julian Pawlowski
f373c01fbb test: remove obsolete lifecycle callback tests
Deleted test_lifecycle_tomorrow_update.py (2 tests) which validated the
now-removed lifecycle callback system.

These tests were rendered obsolete by the removal of the custom lifecycle
callback mechanism in favor of Home Assistant's standard coordinator pattern.

Impact: Test suite reduced from 355 to 349 tests, all passing.
2025-11-22 13:01:30 +00:00
Julian Pawlowski
f2627a5292 fix(period_handlers): normalize flex and min_distance to absolute values
Fixed critical sign convention bug where negative user-facing values
(e.g., peak_price_flex=-20%) weren't normalized for internal calculations,
causing incorrect period filtering.

Changes:
- periods.py: Added abs() normalization for flex and min_distance_from_avg
- core.py: Added comment documenting flex normalization by get_period_config()
- level_filtering.py: Simplified check_interval_criteria() to work with normalized
  positive values only, removed complex negative price handling
- relaxation.py: Removed sign handling since values are pre-normalized

Internal convention:
- User-facing: Best price uses positive (+15%), Peak price uses negative (-20%)
- Internal: Always positive (0.15 or 0.20) with reverse_sort flag for direction

Added comprehensive regression tests:
- test_best_price_e2e.py: Validates Best price periods generate correctly
- test_peak_price_e2e.py: Validates Peak price periods generate correctly
- test_level_filtering.py: Unit tests for flex/distance filter logic

Impact: Peak price periods now generate correctly. Bug caused 100% FLEX
filtering (192/192 intervals blocked) → 0 periods found. Fix ensures
reasonable filtering (~40-50%) with periods successfully generated.
2025-11-22 13:01:01 +00:00
Julian Pawlowski
f128d00c99 test(period): document period calculation testing strategy
Added documentation file explaining why period calculation functions
are tested via integration tests rather than unit tests.

Rationale:
- Period building requires full coordinator context (TimeService, price_context)
- Complex enriched price data with multiple calculated fields
- Helper functions (split_intervals_by_day, calculate_reference_prices)
  are simple transformations that can't fail independently
- Integration tests provide better coverage than mocked unit tests

Testing strategy:
- test_midnight_periods.py: Period calculation across day boundaries
- test_midnight_turnover.py: Cache invalidation and recalculation
- docs/development/period-calculation-theory.md: Algorithm documentation

Impact: Clarifies testing approach for future contributors. Prevents
wasted effort on low-value unit tests for complex integrated functions.
2025-11-22 04:47:09 +00:00
Julian Pawlowski
a85c37e5ca test(time): add boundary tolerance and DST handling tests
Added 40+ tests for TibberPricesTimeService:

Quarter-hour rounding with ±2s tolerance:
- 17 tests covering boundary cases (exact, within tolerance, outside)
- Midnight wrap-around scenarios
- Microsecond precision edge cases

DST handling (23h/25h days):
- Standard day: 96 intervals (24h × 4)
- Spring DST: 92 intervals (23h × 4)
- Fall DST: 100 intervals (25h × 4)
- Note: Full DST tests require Europe/Berlin timezone setup

Day boundaries and interval offsets:
- Yesterday/today/tomorrow boundary calculation
- Interval offset (current/next/previous) with day wrapping
- Time comparison helpers (is_current_interval)

Impact: Validates critical time handling logic. Ensures quarter-hour
rounding works correctly for sensor updates despite HA scheduling jitter.
2025-11-22 04:46:53 +00:00
Julian Pawlowski
91ef2806e5 test(timers): comprehensive timer architecture validation
Added 60+ tests for three-timer architecture:

Timer #1 (API polling): next_api_poll_time calculation
- 8 tests covering timer offset calculation before/after 13:00
- Tests tomorrow data presence logic
- Verifies minute/second offset preservation

Timer #2 (quarter-hour refresh): :00, :15, :30, :45 boundaries
- 10 tests covering registration, cancellation, callback execution
- Verifies exact boundary timing (second=0)
- Tests independence from Timer #3

Timer #3 (minute refresh): :00, :30 every minute
- 6 tests covering 30-second boundary registration
- Verifies timing sensors assignment
- Tests countdown/progress update frequency

Sensor assignment:
- 20+ tests mapping 80+ sensors to correct timers
- Verifies TIME_SENSITIVE and MINUTE_UPDATE constants
- Catches missing/incorrect timer assignments

Impact: Comprehensive validation of timer architecture prevents
regression in entity update scheduling. Documents which sensors
use which timers.
2025-11-22 04:46:30 +00:00
Julian Pawlowski
d1376c8921 test(cleanup): add comprehensive resource cleanup tests
Added 40+ tests verifying memory leak prevention patterns:

- Listener cleanup: Time-sensitive, minute-update, lifecycle callbacks
- Timer cancellation: Quarter-hour, minute timers
- Config entry cleanup: Options update listener via async_on_unload
- Cache invalidation: Config, period, trend caches
- Storage cleanup: Cache files deleted on entry removal

Tests verify cleanup patterns exist in code (not full integration tests
due to complex mocking requirements).

Impact: Documents and tests cleanup contracts for future maintainability.
Prevents memory leaks when entities removed or config changed.
2025-11-22 04:46:11 +00:00
Julian Pawlowski
c7f6843c5b fix(sensors): ensure connection/tomorrow_data/lifecycle consistency
Fixed state inconsistencies during auth errors:

Bug #4: tomorrow_data_available incorrectly returns True during auth failure
- Now returns None (unknown) when coordinator.last_exception is ConfigEntryAuthFailed
- Prevents misleading "data available" when API connection lost

Bug #5: Sensor states inconsistent during error conditions
- connection: False during auth error (even with cached data)
- tomorrow_data_available: None during auth error (cannot verify)
- lifecycle_status: "error" during auth error

Changes:
- binary_sensor/core.py: Check last_exception before evaluating tomorrow data
- tests: 25 integration tests covering all error scenarios

Impact: All three sensors show consistent states during auth errors,
API timeouts, and normal operation. No misleading "available" status
when connection is lost.
2025-11-22 04:45:57 +00:00
Julian Pawlowski
85fe9666a7 feat(coordinator): add atomic midnight turnover coordination
Introduced TibberPricesMidnightHandler to prevent duplicate midnight
turnover when multiple timers fire simultaneously.

Problem: Timer #1 (API poll) and Timer #2 (quarter-hour refresh) both
wake at midnight, each detecting day change and triggering cache clear.
Race condition caused duplicate turnover operations.

Solution:
- Atomic flag coordination: First timer sets flag, subsequent timers skip
- Persistent state survives HA restart (cache stores last_turnover_time)
- Day-boundary detection: Compares current.date() vs last_check.date()
- 13 comprehensive tests covering race conditions and HA restart scenarios

Architecture:
- coordinator/midnight_handler.py: 165 lines, atomic coordination logic
- coordinator/core.py: Integrated handler in coordinator initialization
- coordinator/listeners.py: Delegate midnight check to handler

Impact: Eliminates duplicate cache clears at midnight. Single atomic
turnover operation regardless of how many timers fire simultaneously.
2025-11-22 04:45:41 +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
9a6eb44382 refactor(config): use negative values for Best Price min_distance
Best Price min_distance now uses negative values (-50 to 0) to match
semantic meaning "below average". Peak Price continues using positive
values (0 to 50) for "above average".

Uniform formula: avg * (1 + distance/100) works for both period types.
Sign indicates direction: negative = toward MIN (cheap), positive = toward MAX (expensive).

Changes:
- const.py: DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG = -5 (was 5)
- schemas.py: Best Price range -50 to 0, Peak Price range 0 to 50
- validators.py: Separate validate_best_price_distance_percentage()
- level_filtering.py: Simplified to uniform formula (removed conditionals)
- translations: Separate error messages for Best/Peak distance validation
- tests: 37 comprehensive validator tests (100% coverage)

Impact: Configuration UI now visually represents direction relative to average.
Users see intuitive negative values for "below average" pricing.
2025-11-22 04:44:57 +00:00
Julian Pawlowski
215ac02302 feat(sensors): add lifecycle callback for chart_data_export sensor
chart_data_export now registers lifecycle callback for immediate
updates when coordinator data changes ("fresh" lifecycle state).
Previously only updated via polling intervals.

Changes:
- Register callback in sensor constructor (when entity_key matches)
- Callback triggers async_write_ha_state() on "fresh" lifecycle
- 5 new tests covering callback registration and triggering

Impact: Chart data export updates immediately on API data arrival,
enabling real-time dashboard updates without polling delay.
2025-11-22 04:44:38 +00:00
Julian Pawlowski
49866f26fa fix(coordinator): use coordinator update timestamp for cache validity
Cache validity now checks _last_coordinator_update (within 30min)
instead of _api_calls_today counter. Fixes false "stale" status
when coordinator runs every 15min but cache validation was only
checking API call counter.

Bug #1: Cache validity shows "stale" at 05:57 AM
Bug #2: Cache age calculation incorrect after midnight turnover
Bug #3: get_cache_validity inconsistent with cache_age sensor

Changes:
- Coordinator: Use _last_coordinator_update for cache validation
- Lifecycle: Extract cache validation to dedicated helper function
- Tests: 7 new tests covering midnight scenarios and edge cases

Impact: Cache validity sensor now accurately reflects coordinator
activity, not just explicit API calls. Correctly handles midnight
turnover without false "stale" status.
2025-11-22 04:44:22 +00:00
Julian Pawlowski
f60b5990ae test: add pytest framework and midnight-crossing tests
Set up pytest with Home Assistant support and created 6 tests for
midnight-crossing period logic (5 unit tests + 1 integration test).

Added pytest configuration, test dependencies, test runner script
(./scripts/test), and comprehensive tests for group_periods_by_day()
and midnight turnover consistency.

All tests pass in 0.12s.

Impact: Provides regression testing for midnight-crossing period bugs.
Tests validate periods remain visible across midnight turnover.
2025-11-21 23:47:01 +00:00
Julian Pawlowski
6b2c45d52c refactor: Remove obsolete test files 2025-11-07 23:43:07 +00:00