From aa9a1200b81e31fd75d746adb30d061073fc1591 Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Sun, 12 Apr 2026 14:15:31 +0000 Subject: [PATCH] chore(style): normalize Markdown list indentation across all docs Convert four-space-indented list items (`- item`) to standard two-space (`- item`) in AGENTS.md, CONTRIBUTING.md, README.md, and all Docusaurus documentation pages (developer and user, including versioned snapshots). No content changes. Release-Notes: skip --- AGENTS.md | 1043 +++++++++-------- CONTRIBUTING.md | 34 +- README.md | 88 +- docs/developer/docs/api-reference.md | 131 ++- docs/developer/docs/architecture.md | 143 +-- docs/developer/docs/caching-strategy.md | 132 ++- docs/developer/docs/coding-guidelines.md | 11 +- docs/developer/docs/contributing.md | 25 +- docs/developer/docs/critical-patterns.md | 65 +- docs/developer/docs/debugging.md | 52 +- docs/developer/docs/intro.md | 123 +- docs/developer/docs/performance.md | 17 + .../docs/period-calculation-theory.md | 371 +++--- docs/developer/docs/recorder-optimization.md | 103 +- docs/developer/docs/refactoring-guide.md | 200 ++-- docs/developer/docs/release-management.md | 36 +- docs/developer/docs/repairs-system.md | 32 +- docs/developer/docs/setup.md | 16 +- docs/developer/docs/testing.md | 16 +- docs/developer/docs/timer-architecture.md | 44 +- .../version-v0.21.0/api-reference.md | 131 ++- .../version-v0.21.0/architecture.md | 143 +-- .../version-v0.21.0/caching-strategy.md | 132 ++- .../version-v0.21.0/coding-guidelines.md | 11 +- .../version-v0.21.0/contributing.md | 25 +- .../version-v0.21.0/critical-patterns.md | 65 +- .../version-v0.21.0/debugging.md | 52 +- .../versioned_docs/version-v0.21.0/intro.md | 123 +- .../version-v0.21.0/performance.md | 17 + .../period-calculation-theory.md | 314 +++-- .../version-v0.21.0/recorder-optimization.md | 72 +- .../version-v0.21.0/refactoring-guide.md | 200 ++-- .../version-v0.21.0/release-management.md | 36 +- .../version-v0.21.0/repairs-system.md | 32 +- .../versioned_docs/version-v0.21.0/setup.md | 16 +- .../versioned_docs/version-v0.21.0/testing.md | 16 +- .../version-v0.21.0/timer-architecture.md | 44 +- .../version-v0.22.0/api-reference.md | 131 ++- .../version-v0.22.0/architecture.md | 143 +-- .../version-v0.22.0/caching-strategy.md | 132 ++- .../version-v0.22.0/coding-guidelines.md | 11 +- .../version-v0.22.0/contributing.md | 25 +- .../version-v0.22.0/critical-patterns.md | 65 +- .../version-v0.22.0/debugging.md | 52 +- .../versioned_docs/version-v0.22.0/intro.md | 123 +- .../version-v0.22.0/performance.md | 17 + .../period-calculation-theory.md | 314 +++-- .../version-v0.22.0/recorder-optimization.md | 80 +- .../version-v0.22.0/refactoring-guide.md | 200 ++-- .../version-v0.22.0/release-management.md | 36 +- .../version-v0.22.0/repairs-system.md | 32 +- .../versioned_docs/version-v0.22.0/setup.md | 16 +- .../versioned_docs/version-v0.22.0/testing.md | 16 +- .../version-v0.22.0/timer-architecture.md | 44 +- .../version-v0.22.1/api-reference.md | 131 ++- .../version-v0.22.1/architecture.md | 143 +-- .../version-v0.22.1/caching-strategy.md | 132 ++- .../version-v0.22.1/coding-guidelines.md | 11 +- .../version-v0.22.1/contributing.md | 25 +- .../version-v0.22.1/critical-patterns.md | 65 +- .../version-v0.22.1/debugging.md | 52 +- .../versioned_docs/version-v0.22.1/intro.md | 123 +- .../version-v0.22.1/performance.md | 17 + .../period-calculation-theory.md | 314 +++-- .../version-v0.22.1/recorder-optimization.md | 80 +- .../version-v0.22.1/refactoring-guide.md | 200 ++-- .../version-v0.22.1/release-management.md | 36 +- .../version-v0.22.1/repairs-system.md | 32 +- .../versioned_docs/version-v0.22.1/setup.md | 16 +- .../versioned_docs/version-v0.22.1/testing.md | 16 +- .../version-v0.22.1/timer-architecture.md | 44 +- .../version-v0.23.0/api-reference.md | 131 ++- .../version-v0.23.0/architecture.md | 143 +-- .../version-v0.23.0/caching-strategy.md | 132 ++- .../version-v0.23.0/coding-guidelines.md | 11 +- .../version-v0.23.0/contributing.md | 25 +- .../version-v0.23.0/critical-patterns.md | 65 +- .../version-v0.23.0/debugging.md | 52 +- .../versioned_docs/version-v0.23.0/intro.md | 123 +- .../version-v0.23.0/performance.md | 17 + .../period-calculation-theory.md | 314 +++-- .../version-v0.23.0/recorder-optimization.md | 80 +- .../version-v0.23.0/refactoring-guide.md | 200 ++-- .../version-v0.23.0/release-management.md | 36 +- .../version-v0.23.0/repairs-system.md | 32 +- .../versioned_docs/version-v0.23.0/setup.md | 16 +- .../versioned_docs/version-v0.23.0/testing.md | 16 +- .../version-v0.23.0/timer-architecture.md | 44 +- .../version-v0.23.1/api-reference.md | 131 ++- .../version-v0.23.1/architecture.md | 143 +-- .../version-v0.23.1/caching-strategy.md | 132 ++- .../version-v0.23.1/coding-guidelines.md | 11 +- .../version-v0.23.1/contributing.md | 25 +- .../version-v0.23.1/critical-patterns.md | 65 +- .../version-v0.23.1/debugging.md | 52 +- .../versioned_docs/version-v0.23.1/intro.md | 123 +- .../version-v0.23.1/performance.md | 17 + .../period-calculation-theory.md | 314 +++-- .../version-v0.23.1/recorder-optimization.md | 80 +- .../version-v0.23.1/refactoring-guide.md | 200 ++-- .../version-v0.23.1/release-management.md | 36 +- .../version-v0.23.1/repairs-system.md | 32 +- .../versioned_docs/version-v0.23.1/setup.md | 16 +- .../versioned_docs/version-v0.23.1/testing.md | 16 +- .../version-v0.23.1/timer-architecture.md | 44 +- .../version-v0.24.0/api-reference.md | 131 ++- .../version-v0.24.0/architecture.md | 143 +-- .../version-v0.24.0/caching-strategy.md | 132 ++- .../version-v0.24.0/coding-guidelines.md | 11 +- .../version-v0.24.0/contributing.md | 25 +- .../version-v0.24.0/critical-patterns.md | 65 +- .../version-v0.24.0/debugging.md | 52 +- .../versioned_docs/version-v0.24.0/intro.md | 123 +- .../version-v0.24.0/performance.md | 17 + .../period-calculation-theory.md | 314 +++-- .../version-v0.24.0/recorder-optimization.md | 80 +- .../version-v0.24.0/refactoring-guide.md | 200 ++-- .../version-v0.24.0/release-management.md | 36 +- .../version-v0.24.0/repairs-system.md | 32 +- .../versioned_docs/version-v0.24.0/setup.md | 16 +- .../versioned_docs/version-v0.24.0/testing.md | 16 +- .../version-v0.24.0/timer-architecture.md | 44 +- .../version-v0.27.0/api-reference.md | 131 ++- .../version-v0.27.0/architecture.md | 143 +-- .../version-v0.27.0/caching-strategy.md | 132 ++- .../version-v0.27.0/coding-guidelines.md | 11 +- .../version-v0.27.0/contributing.md | 25 +- .../version-v0.27.0/critical-patterns.md | 65 +- .../version-v0.27.0/debugging.md | 52 +- .../versioned_docs/version-v0.27.0/intro.md | 123 +- .../version-v0.27.0/performance.md | 17 + .../period-calculation-theory.md | 314 +++-- .../version-v0.27.0/recorder-optimization.md | 80 +- .../version-v0.27.0/refactoring-guide.md | 200 ++-- .../version-v0.27.0/release-management.md | 36 +- .../version-v0.27.0/repairs-system.md | 32 +- .../versioned_docs/version-v0.27.0/setup.md | 16 +- .../versioned_docs/version-v0.27.0/testing.md | 16 +- .../version-v0.27.0/timer-architecture.md | 44 +- .../version-v0.28.0/api-reference.md | 131 ++- .../version-v0.28.0/architecture.md | 143 +-- .../version-v0.28.0/caching-strategy.md | 132 ++- .../version-v0.28.0/coding-guidelines.md | 11 +- .../version-v0.28.0/contributing.md | 25 +- .../version-v0.28.0/critical-patterns.md | 65 +- .../version-v0.28.0/debugging.md | 52 +- .../versioned_docs/version-v0.28.0/intro.md | 123 +- .../version-v0.28.0/performance.md | 17 + .../period-calculation-theory.md | 375 +++--- .../version-v0.28.0/recorder-optimization.md | 103 +- .../version-v0.28.0/refactoring-guide.md | 200 ++-- .../version-v0.28.0/release-management.md | 36 +- .../version-v0.28.0/repairs-system.md | 32 +- .../versioned_docs/version-v0.28.0/setup.md | 16 +- .../versioned_docs/version-v0.28.0/testing.md | 16 +- .../version-v0.28.0/timer-architecture.md | 44 +- .../version-v0.29.0/api-reference.md | 131 ++- .../version-v0.29.0/architecture.md | 143 +-- .../version-v0.29.0/caching-strategy.md | 132 ++- .../version-v0.29.0/coding-guidelines.md | 11 +- .../version-v0.29.0/contributing.md | 25 +- .../version-v0.29.0/critical-patterns.md | 65 +- .../version-v0.29.0/debugging.md | 52 +- .../versioned_docs/version-v0.29.0/intro.md | 123 +- .../version-v0.29.0/performance.md | 17 + .../period-calculation-theory.md | 375 +++--- .../version-v0.29.0/recorder-optimization.md | 103 +- .../version-v0.29.0/refactoring-guide.md | 200 ++-- .../version-v0.29.0/release-management.md | 36 +- .../version-v0.29.0/repairs-system.md | 32 +- .../versioned_docs/version-v0.29.0/setup.md | 16 +- .../versioned_docs/version-v0.29.0/testing.md | 16 +- .../version-v0.29.0/timer-architecture.md | 44 +- .../version-v0.30.0/api-reference.md | 131 ++- .../version-v0.30.0/architecture.md | 143 +-- .../version-v0.30.0/caching-strategy.md | 132 ++- .../version-v0.30.0/coding-guidelines.md | 11 +- .../version-v0.30.0/contributing.md | 25 +- .../version-v0.30.0/critical-patterns.md | 65 +- .../version-v0.30.0/debugging.md | 52 +- .../versioned_docs/version-v0.30.0/intro.md | 123 +- .../version-v0.30.0/performance.md | 17 + .../period-calculation-theory.md | 375 +++--- .../version-v0.30.0/recorder-optimization.md | 103 +- .../version-v0.30.0/refactoring-guide.md | 200 ++-- .../version-v0.30.0/release-management.md | 36 +- .../version-v0.30.0/repairs-system.md | 32 +- .../versioned_docs/version-v0.30.0/setup.md | 16 +- .../versioned_docs/version-v0.30.0/testing.md | 16 +- .../version-v0.30.0/timer-architecture.md | 44 +- .../version-v0.21.0-sidebars.json | 9 +- .../version-v0.22.0-sidebars.json | 9 +- .../version-v0.22.1-sidebars.json | 9 +- .../version-v0.23.0-sidebars.json | 9 +- .../version-v0.23.1-sidebars.json | 9 +- .../version-v0.24.0-sidebars.json | 9 +- .../version-v0.27.0-sidebars.json | 9 +- .../version-v0.28.0-sidebars.json | 9 +- .../version-v0.29.0-sidebars.json | 113 +- .../version-v0.30.0-sidebars.json | 9 +- .../versioned_docs/version-v0.21.0/actions.md | 57 +- .../version-v0.21.0/automation-examples.md | 289 ++--- .../version-v0.21.0/chart-examples.md | 55 +- .../version-v0.21.0/concepts.md | 4 + .../version-v0.21.0/dashboard-examples.md | 169 +-- .../version-v0.21.0/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.21.0/faq.md | 53 +- .../version-v0.21.0/glossary.md | 1 + .../version-v0.21.0/icon-colors.md | 68 +- .../versioned_docs/version-v0.21.0/intro.md | 48 +- .../version-v0.21.0/period-calculation.md | 172 +-- .../versioned_docs/version-v0.21.0/sensors.md | 44 +- .../versioned_docs/version-v0.22.0/actions.md | 59 +- .../version-v0.22.0/automation-examples.md | 289 ++--- .../version-v0.22.0/chart-examples.md | 55 +- .../version-v0.22.0/concepts.md | 4 + .../version-v0.22.0/dashboard-examples.md | 169 +-- .../version-v0.22.0/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.22.0/faq.md | 57 +- .../version-v0.22.0/glossary.md | 1 + .../version-v0.22.0/icon-colors.md | 68 +- .../versioned_docs/version-v0.22.0/intro.md | 48 +- .../version-v0.22.0/period-calculation.md | 174 +-- .../versioned_docs/version-v0.22.0/sensors.md | 44 +- .../versioned_docs/version-v0.22.1/actions.md | 59 +- .../version-v0.22.1/automation-examples.md | 289 ++--- .../version-v0.22.1/chart-examples.md | 55 +- .../version-v0.22.1/concepts.md | 4 + .../version-v0.22.1/dashboard-examples.md | 169 +-- .../version-v0.22.1/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.22.1/faq.md | 57 +- .../version-v0.22.1/glossary.md | 1 + .../version-v0.22.1/icon-colors.md | 68 +- .../versioned_docs/version-v0.22.1/intro.md | 48 +- .../version-v0.22.1/period-calculation.md | 174 +-- .../versioned_docs/version-v0.22.1/sensors.md | 44 +- .../versioned_docs/version-v0.23.0/actions.md | 59 +- .../version-v0.23.0/automation-examples.md | 289 ++--- .../version-v0.23.0/chart-examples.md | 55 +- .../version-v0.23.0/concepts.md | 4 + .../version-v0.23.0/configuration.md | 7 + .../version-v0.23.0/dashboard-examples.md | 169 +-- .../version-v0.23.0/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.23.0/faq.md | 57 +- .../version-v0.23.0/glossary.md | 1 + .../version-v0.23.0/icon-colors.md | 68 +- .../versioned_docs/version-v0.23.0/intro.md | 48 +- .../version-v0.23.0/period-calculation.md | 174 +-- .../versioned_docs/version-v0.23.0/sensors.md | 207 ++-- .../versioned_docs/version-v0.23.1/actions.md | 59 +- .../version-v0.23.1/automation-examples.md | 289 ++--- .../version-v0.23.1/chart-examples.md | 55 +- .../version-v0.23.1/concepts.md | 4 + .../version-v0.23.1/configuration.md | 7 + .../version-v0.23.1/dashboard-examples.md | 169 +-- .../version-v0.23.1/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.23.1/faq.md | 57 +- .../version-v0.23.1/glossary.md | 1 + .../version-v0.23.1/icon-colors.md | 68 +- .../versioned_docs/version-v0.23.1/intro.md | 48 +- .../version-v0.23.1/period-calculation.md | 174 +-- .../versioned_docs/version-v0.23.1/sensors.md | 207 ++-- .../versioned_docs/version-v0.24.0/actions.md | 59 +- .../version-v0.24.0/automation-examples.md | 289 ++--- .../version-v0.24.0/chart-examples.md | 55 +- .../version-v0.24.0/concepts.md | 4 + .../version-v0.24.0/configuration.md | 7 + .../version-v0.24.0/dashboard-examples.md | 169 +-- .../version-v0.24.0/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.24.0/faq.md | 57 +- .../version-v0.24.0/glossary.md | 1 + .../version-v0.24.0/icon-colors.md | 68 +- .../versioned_docs/version-v0.24.0/intro.md | 48 +- .../version-v0.24.0/period-calculation.md | 174 +-- .../versioned_docs/version-v0.24.0/sensors.md | 207 ++-- .../versioned_docs/version-v0.27.0/actions.md | 59 +- .../version-v0.27.0/automation-examples.md | 62 +- .../version-v0.27.0/chart-examples.md | 55 +- .../version-v0.27.0/concepts.md | 4 + .../version-v0.27.0/configuration.md | 85 +- .../version-v0.27.0/dashboard-examples.md | 169 +-- .../version-v0.27.0/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.27.0/faq.md | 57 +- .../version-v0.27.0/glossary.md | 1 + .../version-v0.27.0/icon-colors.md | 68 +- .../versioned_docs/version-v0.27.0/intro.md | 48 +- .../version-v0.27.0/period-calculation.md | 174 +-- .../versioned_docs/version-v0.27.0/sensors.md | 279 ++--- .../versioned_docs/version-v0.28.0/actions.md | 59 +- .../version-v0.28.0/automation-examples.md | 62 +- .../version-v0.28.0/chart-examples.md | 55 +- .../version-v0.28.0/concepts.md | 4 + .../version-v0.28.0/configuration.md | 85 +- .../version-v0.28.0/dashboard-examples.md | 169 +-- .../version-v0.28.0/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.28.0/faq.md | 57 +- .../version-v0.28.0/glossary.md | 1 + .../version-v0.28.0/icon-colors.md | 68 +- .../versioned_docs/version-v0.28.0/intro.md | 48 +- .../version-v0.28.0/period-calculation.md | 215 ++-- .../versioned_docs/version-v0.28.0/sensors.md | 279 ++--- .../versioned_docs/version-v0.29.0/actions.md | 59 +- .../version-v0.29.0/automation-examples.md | 84 +- .../version-v0.29.0/chart-examples.md | 55 +- .../version-v0.29.0/concepts.md | 4 + .../version-v0.29.0/configuration.md | 122 +- .../version-v0.29.0/dashboard-examples.md | 169 +-- .../version-v0.29.0/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.29.0/faq.md | 57 +- .../version-v0.29.0/glossary.md | 6 +- .../version-v0.29.0/icon-colors.md | 68 +- .../version-v0.29.0/installation.md | 28 +- .../versioned_docs/version-v0.29.0/intro.md | 48 +- .../version-v0.29.0/period-calculation.md | 215 ++-- .../versioned_docs/version-v0.29.0/sensors.md | 572 ++++----- .../versioned_docs/version-v0.30.0/actions.md | 71 +- .../version-v0.30.0/automation-examples.md | 84 +- .../version-v0.30.0/chart-examples.md | 55 +- .../version-v0.30.0/concepts.md | 4 + .../version-v0.30.0/configuration.md | 122 +- .../version-v0.30.0/dashboard-examples.md | 169 +-- .../version-v0.30.0/dynamic-icons.md | 60 +- .../versioned_docs/version-v0.30.0/faq.md | 57 +- .../version-v0.30.0/glossary.md | 6 +- .../version-v0.30.0/icon-colors.md | 68 +- .../version-v0.30.0/installation.md | 28 +- .../versioned_docs/version-v0.30.0/intro.md | 48 +- .../version-v0.30.0/period-calculation.md | 215 ++-- .../versioned_docs/version-v0.30.0/sensors.md | 633 +++++----- .../version-v0.21.0-sidebars.json | 24 +- .../version-v0.22.0-sidebars.json | 24 +- .../version-v0.22.1-sidebars.json | 24 +- .../version-v0.23.0-sidebars.json | 24 +- .../version-v0.23.1-sidebars.json | 24 +- .../version-v0.24.0-sidebars.json | 24 +- .../version-v0.27.0-sidebars.json | 24 +- .../version-v0.28.0-sidebars.json | 24 +- .../version-v0.29.0-sidebars.json | 116 +- .../version-v0.30.0-sidebars.json | 24 +- 339 files changed, 16987 insertions(+), 12955 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8791e7d..2126d77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,10 +4,10 @@ This is a **Home Assistant custom component** for Tibber electricity price data, ## Documentation Metadata -- **Last Major Update**: 2025-01-21 -- **Last Architecture Review**: 2025-01-21 (Phase 1: Added TypedDict documentation system, improved BaseCalculator with 8 helper methods. Phase 2: Documented Import Architecture - Hybrid Pattern (Trend/Volatility build own attributes), verified no circular dependencies, confirmed optimal TYPE_CHECKING usage across all 8 calculators.) -- **Last Code Example Cleanup**: 2025-11-18 (Removed redundant implementation details from AGENTS.md, added guidelines for when to include code examples) -- **Documentation Status**: ✅ Current (verified against codebase) +- **Last Major Update**: 2025-01-21 +- **Last Architecture Review**: 2025-01-21 (Phase 1: Added TypedDict documentation system, improved BaseCalculator with 8 helper methods. Phase 2: Documented Import Architecture - Hybrid Pattern (Trend/Volatility build own attributes), verified no circular dependencies, confirmed optimal TYPE_CHECKING usage across all 8 calculators.) +- **Last Code Example Cleanup**: 2025-11-18 (Removed redundant implementation details from AGENTS.md, added guidelines for when to include code examples) +- **Documentation Status**: ✅ Current (verified against codebase) _Note: When proposing significant updates to this file, update the metadata above with the new date and brief description of changes._ @@ -21,22 +21,22 @@ When working with the codebase, Copilot MUST actively maintain consistency betwe **Documentation Organization:** -- **This file** (`AGENTS.md`): AI/Developer long-term memory, patterns, conventions -- **`docs/user/`**: Docusaurus site for end-users (installation, configuration, usage examples) - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **`docs/developer/`**: Docusaurus site for contributors (architecture, development guides) - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **`README.md`**: Project overview with links to documentation sites +- **This file** (`AGENTS.md`): AI/Developer long-term memory, patterns, conventions +- **`docs/user/`**: Docusaurus site for end-users (installation, configuration, usage examples) + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **`docs/developer/`**: Docusaurus site for contributors (architecture, development guides) + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **`README.md`**: Project overview with links to documentation sites **Automatic Inconsistency Detection:** -- When code changes affect documented patterns, examples, file paths, function names, or architectural decisions **in this file**, IMMEDIATELY flag the inconsistency -- If a documented function is renamed, moved, or deleted → suggest documentation update -- If file structure changes (files moved/renamed/deleted) → suggest path updates -- If implementation patterns change (e.g., new translation structure, different caching approach) → suggest pattern documentation update -- If new learnings emerge during debugging or development → suggest adding to documentation +- When code changes affect documented patterns, examples, file paths, function names, or architectural decisions **in this file**, IMMEDIATELY flag the inconsistency +- If a documented function is renamed, moved, or deleted → suggest documentation update +- If file structure changes (files moved/renamed/deleted) → suggest path updates +- If implementation patterns change (e.g., new translation structure, different caching approach) → suggest pattern documentation update +- If new learnings emerge during debugging or development → suggest adding to documentation **Documentation Update Process:** @@ -48,19 +48,18 @@ When working with the codebase, Copilot MUST actively maintain consistency betwe **When to discuss in chat vs. direct file changes:** -- **Make direct changes when:** +- **Make direct changes when:** + - Clear, straightforward task (fix bug, add function, update config) + - Single approach is obvious + - User request is specific ("add X", "change Y to Z") + - Quick iteration is needed (user can review diff and iterate) - - Clear, straightforward task (fix bug, add function, update config) - - Single approach is obvious - - User request is specific ("add X", "change Y to Z") - - Quick iteration is needed (user can review diff and iterate) - -- **Discuss/show in chat first when:** - - Multiple valid approaches exist (architectural decision) - - Significant refactoring affecting many files - - Unclear requirements need clarification - - Trade-offs need discussion (performance vs. readability, etc.) - - User asks open-ended question ("how should we...", "what's the best way...") +- **Discuss/show in chat first when:** + - Multiple valid approaches exist (architectural decision) + - Significant refactoring affecting many files + - Unclear requirements need clarification + - Trade-offs need discussion (performance vs. readability, etc.) + - User asks open-ended question ("how should we...", "what's the best way...") **Goal:** Save time. File edits with VS Code tracking are fast for simple changes. Chat discussion is better for decisions requiring input before committing to an approach. @@ -72,19 +71,19 @@ When working with the codebase, Copilot MUST actively maintain consistency betwe ✅ **DO include examples for:** -- **Architectural patterns** - Show WHY a design decision was made (e.g., "direct method pattern vs Callable pattern") -- **Non-obvious patterns** - Illustrate unusual HA-specific patterns not documented elsewhere (e.g., selector translation structure) -- **Decision rationale** - Demonstrate trade-offs between approaches (e.g., performance comparison with metrics) -- **Configuration patterns** - Show structure of config files when format is critical (e.g., git-cliff.toml template) -- **Best practices vs anti-patterns** - Side-by-side comparison of ✅ correct vs ❌ wrong approaches +- **Architectural patterns** - Show WHY a design decision was made (e.g., "direct method pattern vs Callable pattern") +- **Non-obvious patterns** - Illustrate unusual HA-specific patterns not documented elsewhere (e.g., selector translation structure) +- **Decision rationale** - Demonstrate trade-offs between approaches (e.g., performance comparison with metrics) +- **Configuration patterns** - Show structure of config files when format is critical (e.g., git-cliff.toml template) +- **Best practices vs anti-patterns** - Side-by-side comparison of ✅ correct vs ❌ wrong approaches ❌ **DON'T include examples for:** -- **Implementation details** - Code that duplicates what's in actual source files (e.g., full function implementations) -- **API usage** - Standard library or HA API calls that are documented elsewhere (just reference the actual files) -- **Entity definitions** - Complete SensorEntityDescription examples (just describe the pattern) -- **Translation JSON** - Full translation file examples (just show the key structure pattern) -- **Service schemas** - Complete schema definitions (reference services.py instead) +- **Implementation details** - Code that duplicates what's in actual source files (e.g., full function implementations) +- **API usage** - Standard library or HA API calls that are documented elsewhere (just reference the actual files) +- **Entity definitions** - Complete SensorEntityDescription examples (just describe the pattern) +- **Translation JSON** - Full translation file examples (just show the key structure pattern) +- **Service schemas** - Complete schema definitions (reference services.py instead) **Style for code examples:** @@ -138,31 +137,31 @@ This prevents AGENTS.md from becoming outdated when code evolves, while still pr 🔴 **HIGH Confidence** - Factual inconsistencies (flag immediately): -- ✅ Documented function/class renamed, moved, or deleted -- ✅ File paths changed (files moved/renamed/deleted) -- ✅ Example code references non-existent code -- ✅ Breaking changes to documented APIs or patterns +- ✅ Documented function/class renamed, moved, or deleted +- ✅ File paths changed (files moved/renamed/deleted) +- ✅ Example code references non-existent code +- ✅ Breaking changes to documented APIs or patterns 🟡 **MEDIUM Confidence** - Possible changes (ask for clarification): -- ✅ Implementation pattern changed (might be intentional refactor) -- ✅ New approach observed alongside documented old approach (unclear which is preferred) -- ✅ Documented pattern still works but seems outdated +- ✅ Implementation pattern changed (might be intentional refactor) +- ✅ New approach observed alongside documented old approach (unclear which is preferred) +- ✅ Documented pattern still works but seems outdated 🟢 **LOW Confidence** - Suggestions for additions (propose when valuable): -- ✅ New architectural pattern discovered during debugging (like the selector translation structure fix) -- ✅ Important learnings that would help future sessions -- ✅ User expressed wish for documentation -- ✅ HA best practice learned that applies to this project +- ✅ New architectural pattern discovered during debugging (like the selector translation structure fix) +- ✅ Important learnings that would help future sessions +- ✅ User expressed wish for documentation +- ✅ HA best practice learned that applies to this project **Do NOT Propose Updates For:** -- ❌ Temporary debugging code or experimental changes -- ❌ Minor implementation details that don't affect understanding -- ❌ Private helper function internals (unless part of documented pattern) -- ❌ TODO comments (unless they represent architectural decisions) -- ❌ Variable names or internal state (unless part of public API) +- ❌ Temporary debugging code or experimental changes +- ❌ Minor implementation details that don't affect understanding +- ❌ Private helper function internals (unless part of documented pattern) +- ❌ TODO comments (unless they represent architectural decisions) +- ❌ Variable names or internal state (unless part of public API) **Update Proposal Format:** Include confidence level and impact in proposals: @@ -190,44 +189,39 @@ This ensures the documentation stays accurate and useful as the codebase evolves Create a detailed plan when: -- 🔴 **Major refactoring** (>5 files, >500 lines changed) -- 🔴 **Architectural changes** (new packages, module restructuring) -- 🔴 **Breaking changes** (API changes, config format migrations) -- 🟡 **Complex features** (multiple moving parts, unclear best approach) +- 🔴 **Major refactoring** (>5 files, >500 lines changed) +- 🔴 **Architectural changes** (new packages, module restructuring) +- 🔴 **Breaking changes** (API changes, config format migrations) +- 🟡 **Complex features** (multiple moving parts, unclear best approach) Skip planning for: -- 🟢 Bug fixes (straightforward, <100 lines) -- 🟢 Small features (<3 files, clear approach) -- 🟢 Documentation updates -- 🟢 Cosmetic changes (formatting, renaming) +- 🟢 Bug fixes (straightforward, <100 lines) +- 🟢 Small features (<3 files, clear approach) +- 🟢 Documentation updates +- 🟢 Cosmetic changes (formatting, renaming) **Planning Document Lifecycle:** 1. **Planning Phase** (WIP in `/planning/`) - - Create `planning/-refactoring-plan.md` - Iterate freely (git-ignored, no commit pressure) - AI can help refine without polluting git history - Multiple revisions until plan is solid 2. **Implementation Phase** (Active work) - - Use plan as reference during coding - Update plan if issues discovered - Track progress through phases - Test after each phase 3. **Completion Phase** (After implementation) - - **Option A**: Move to `docs/development/` if lasting value - - Example: `planning/module-splitting-plan.md` → `docs/development/module-splitting-plan.md` - Update status to "✅ COMPLETED" - Commit as historical reference - **Option B**: Delete if superseded - - Plan served its purpose - Code and AGENTS.md are source of truth @@ -246,34 +240,34 @@ Skip planning for: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` **Example**: See `docs/development/module-splitting-plan.md` for a completed plan (moved from `planning/` after successful implementation). @@ -290,18 +284,18 @@ After successful refactoring: ✅ **DO:** -- Iterate freely in `/planning/` (git-ignored) -- Break complex changes into clear phases -- Document file lifecycle explicitly -- Include code examples and patterns -- Plan testing after each phase +- Iterate freely in `/planning/` (git-ignored) +- Break complex changes into clear phases +- Document file lifecycle explicitly +- Include code examples and patterns +- Plan testing after each phase ❌ **DON'T:** -- Start coding before plan is solid -- Skip the "Why?" section -- Commit `/planning/` files (they're ignored!) -- Over-plan trivial changes +- Start coding before plan is solid +- Skip the "Why?" section +- Commit `/planning/` files (they're ignored!) +- Over-plan trivial changes ## File Organization and Structure Policy @@ -310,64 +304,67 @@ After successful refactoring: **Root Directory (`custom_components/tibber_prices/`):** **✅ ALLOWED in root:** -- Platform modules: `__init__.py`, `sensor.py` (deprecated, now `sensor/`), `binary_sensor.py` (deprecated, now `binary_sensor/`), future platforms -- Core integration files: `const.py`, `manifest.json`, `services.yaml`, `diagnostics.py`, `data.py`, `migrations.py` -- Translation directories: `translations/`, `custom_translations/` -- Brand images: `brand/` (icon.png, dark_icon.png, logo.png, dark_logo.png + `@2x` variants) — served via HA brands proxy API (HA ≥ 2026.4), silently ignored on older versions + +- Platform modules: `__init__.py`, `sensor.py` (deprecated, now `sensor/`), `binary_sensor.py` (deprecated, now `binary_sensor/`), future platforms +- Core integration files: `const.py`, `manifest.json`, `services.yaml`, `diagnostics.py`, `data.py`, `migrations.py` +- Translation directories: `translations/`, `custom_translations/` +- Brand images: `brand/` (icon.png, dark_icon.png, logo.png, dark_logo.png + `@2x` variants) — served via HA brands proxy API (HA ≥ 2026.4), silently ignored on older versions **❌ PROHIBITED in root:** -- Utility modules (use `/utils/` package instead) -- Helper functions (use `/utils/` or appropriate package) -- Data transformation logic (use `/utils/` or `/coordinator/`) -- Any `*_utils.py` or `*_helpers.py` files + +- Utility modules (use `/utils/` package instead) +- Helper functions (use `/utils/` or appropriate package) +- Data transformation logic (use `/utils/` or `/coordinator/`) +- Any `*_utils.py` or `*_helpers.py` files **Organized Packages:** 1. **`/utils/`** - Pure data transformation functions (stateless) - - `average.py` - Average and time-window calculations - - `price.py` - Price enrichment, volatility, rating calculations - - **Pattern**: Import as `from ..utils.average import function_name` + - `average.py` - Average and time-window calculations + - `price.py` - Price enrichment, volatility, rating calculations + - **Pattern**: Import as `from ..utils.average import function_name` 2. **`/entity_utils/`** - Entity-specific utilities - - `icons.py` - Dynamic icon selection logic - - `colors.py` - Icon color mapping - - `attributes.py` - Common attribute builders - - **Pattern**: Import as `from ..entity_utils import function_name` + - `icons.py` - Dynamic icon selection logic + - `colors.py` - Icon color mapping + - `attributes.py` - Common attribute builders + - **Pattern**: Import as `from ..entity_utils import function_name` 3. **`/coordinator/`** - DataUpdateCoordinator and related logic - - `core.py` - Main coordinator class - - `cache.py` - Persistent storage handling - - `data_transformation.py` - Raw data → enriched data - - `period_handlers/` - Period calculation sub-package - - **Pattern**: Coordinator-specific implementations + - `core.py` - Main coordinator class + - `cache.py` - Persistent storage handling + - `data_transformation.py` - Raw data → enriched data + - `period_handlers/` - Period calculation sub-package + - **Pattern**: Coordinator-specific implementations 4. **`/sensor/`** - Sensor platform package - - `core.py` - Entity class (1,268 lines - manages 80+ sensor types) - - `definitions.py` - Entity descriptions - - `helpers.py` - Sensor-specific helpers - - `calculators/` - Value calculation package (8 specialized calculators, 1,838 lines) - - `attributes/` - Attribute builders package (8 specialized modules, 1,209 lines) - - **Pattern**: Calculator Pattern (business logic separated from presentation) - - **Architecture**: Two-tier (Calculators handle computation → Attributes handle state presentation) + - `core.py` - Entity class (1,268 lines - manages 80+ sensor types) + - `definitions.py` - Entity descriptions + - `helpers.py` - Sensor-specific helpers + - `calculators/` - Value calculation package (8 specialized calculators, 1,838 lines) + - `attributes/` - Attribute builders package (8 specialized modules, 1,209 lines) + - **Pattern**: Calculator Pattern (business logic separated from presentation) + - **Architecture**: Two-tier (Calculators handle computation → Attributes handle state presentation) 5. **`/binary_sensor/`** - Binary sensor platform package - - Same structure as `/sensor/` + - Same structure as `/sensor/` 6. **`/config_flow_handlers/`** - Configuration flow package - - `user_flow.py` - Initial setup flow - - `subentry_flow.py` - Add additional homes - - `options_flow.py` - Reconfiguration - - `schemas.py` - Form schemas - - `validators.py` - Input validation + - `user_flow.py` - Initial setup flow + - `subentry_flow.py` - Add additional homes + - `options_flow.py` - Reconfiguration + - `schemas.py` - Form schemas + - `validators.py` - Input validation 7. **`/api/`** - External API communication - - `client.py` - GraphQL client - - `queries.py` - Query definitions - - `exceptions.py` - API-specific exceptions + - `client.py` - GraphQL client + - `queries.py` - Query definitions + - `exceptions.py` - API-specific exceptions **When Adding New Files:** **Before creating a new file in root, ask:** + 1. Is this a new HA platform? → OK in root (e.g., `switch.py`, `number.py`) 2. Is this a utility/helper? → Goes in `/utils/` or `/entity_utils/` 3. Is this coordinator-related? → Goes in `/coordinator/` @@ -388,9 +385,8 @@ After successful refactoring: **Key Patterns:** -- **Dual translation system**: Standard HA translations in `/translations/` (config flow, UI strings per HA schema), supplemental in `/custom_translations/` (entity descriptions not supported by HA schema). Both must stay in sync. Use `async_load_translations()` and `async_load_standard_translations()` from `const.py`. When to use which: `/translations/` is bound to official HA schema requirements; anything else goes in `/custom_translations/` (requires manual translation loading). **Schema reference**: `/schemas/json/translation_schema.json` provides the structure for `/translations/*.json` files based on [HA's translation documentation](https://developers.home-assistant.io/docs/internationalization/core). - - - **Select selector translations**: Use `selector.{translation_key}.options.{value}` structure (NOT `selector.select.{translation_key}`). Translation keys map to JSON in `/translations/*.json` following the HA schema structure. +- **Dual translation system**: Standard HA translations in `/translations/` (config flow, UI strings per HA schema), supplemental in `/custom_translations/` (entity descriptions not supported by HA schema). Both must stay in sync. Use `async_load_translations()` and `async_load_standard_translations()` from `const.py`. When to use which: `/translations/` is bound to official HA schema requirements; anything else goes in `/custom_translations/` (requires manual translation loading). **Schema reference**: `/schemas/json/translation_schema.json` provides the structure for `/translations/*.json` files based on [HA's translation documentation](https://developers.home-assistant.io/docs/internationalization/core). + - **Select selector translations**: Use `selector.{translation_key}.options.{value}` structure (NOT `selector.select.{translation_key}`). Translation keys map to JSON in `/translations/*.json` following the HA schema structure. **CRITICAL Rules:** - When using `translation_key`, pass options as **plain string list**, NOT `SelectOptionDict` @@ -400,36 +396,36 @@ After successful refactoring: See `config_flow/schemas.py` for implementation examples. -- **Price data enrichment**: All quarter-hourly price intervals get augmented with `trailing_avg_24h`, `difference`, and `rating_level` fields via `enrich_price_info_with_differences()` in `utils/price.py`. This adds statistical analysis (24h trailing average, percentage difference from average, rating classification) to each 15-minute interval. See `utils/price.py` for enrichment logic. -- **Sensor organization (refactored Nov 2025)**: The `sensor/` package uses **Calculator Pattern** for separation of concerns: - - **Calculator Package** (`sensor/calculators/`): 8 specialized calculators handle business logic (1,838 lines total) - - `base.py` - Abstract BaseCalculator with coordinator access - - `interval.py` - Single interval calculations (current/next/previous) - - `rolling_hour.py` - 5-interval rolling windows - - `daily_stat.py` - Calendar day min/max/avg statistics - - `window_24h.py` - Trailing/leading 24h windows - - `volatility.py` - Price volatility analysis - - `trend.py` - Complex trend analysis with caching (640 lines) - - `timing.py` - Best/peak price period timing - - `metadata.py` - Home/metering metadata - - **Attributes Package** (`sensor/attributes/`): 8 specialized modules handle state presentation (1,209 lines total) - - Modules match calculator types: `interval.py`, `daily_stat.py`, `window_24h.py`, `volatility.py`, `trend.py`, `timing.py`, `future.py`, `metadata.py` - - `__init__.py` - Routing logic + unified builders (`build_sensor_attributes`, `build_extra_state_attributes`) - - **Core Entity** (`sensor/core.py`): 1,268 lines managing 80+ sensor types - - Instantiates all calculators in `__init__` - - Delegates value calculations to appropriate calculator - - Uses unified handler methods: `_get_interval_value()`, `_get_rolling_hour_value()`, `_get_daily_stat_value()`, `_get_24h_window_value()` - - Handler mapping dictionary routes entity keys to value getters - - **Architecture Benefits**: 42% line reduction in core.py (2,170 → 1,268 lines), clear separation of concerns, improved testability, reusable components - - **See "Common Tasks" section** for detailed patterns and examples -- **Quarter-hour precision**: Entities update on 00/15/30/45-minute boundaries via `schedule_quarter_hour_refresh()` in `coordinator/listeners.py`, not just on data fetch intervals. Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` for absolute-time scheduling. Smart boundary tolerance (±2 seconds) in `sensor/helpers.py` → `round_to_nearest_quarter_hour()` handles HA scheduling jitter: if HA triggers at 14:59:58 → rounds to 15:00:00 (next interval), if HA restarts at 14:59:30 → stays at 14:45:00 (current interval). This ensures current price sensors update without waiting for the next API poll, while preventing premature data display during normal operation. -- **Currency handling**: Multi-currency support with base/sub units (e.g., EUR/ct, NOK/øre) via `get_currency_info()` and `format_price_unit_*()` in `const.py`. -- **Intelligent caching strategy**: Minimizes API calls while ensuring data freshness: - - User data cached for 24h (rarely changes) - - Price data validated against calendar day - cleared on midnight turnover to force fresh fetch - - Cache survives HA restarts via `Store` persistence - - API polling intensifies only when tomorrow's data expected (afternoons) - - Stale cache detection via `_is_cache_valid()` prevents using yesterday's data as today's +- **Price data enrichment**: All quarter-hourly price intervals get augmented with `trailing_avg_24h`, `difference`, and `rating_level` fields via `enrich_price_info_with_differences()` in `utils/price.py`. This adds statistical analysis (24h trailing average, percentage difference from average, rating classification) to each 15-minute interval. See `utils/price.py` for enrichment logic. +- **Sensor organization (refactored Nov 2025)**: The `sensor/` package uses **Calculator Pattern** for separation of concerns: + - **Calculator Package** (`sensor/calculators/`): 8 specialized calculators handle business logic (1,838 lines total) + - `base.py` - Abstract BaseCalculator with coordinator access + - `interval.py` - Single interval calculations (current/next/previous) + - `rolling_hour.py` - 5-interval rolling windows + - `daily_stat.py` - Calendar day min/max/avg statistics + - `window_24h.py` - Trailing/leading 24h windows + - `volatility.py` - Price volatility analysis + - `trend.py` - Complex trend analysis with caching (640 lines) + - `timing.py` - Best/peak price period timing + - `metadata.py` - Home/metering metadata + - **Attributes Package** (`sensor/attributes/`): 8 specialized modules handle state presentation (1,209 lines total) + - Modules match calculator types: `interval.py`, `daily_stat.py`, `window_24h.py`, `volatility.py`, `trend.py`, `timing.py`, `future.py`, `metadata.py` + - `__init__.py` - Routing logic + unified builders (`build_sensor_attributes`, `build_extra_state_attributes`) + - **Core Entity** (`sensor/core.py`): 1,268 lines managing 80+ sensor types + - Instantiates all calculators in `__init__` + - Delegates value calculations to appropriate calculator + - Uses unified handler methods: `_get_interval_value()`, `_get_rolling_hour_value()`, `_get_daily_stat_value()`, `_get_24h_window_value()` + - Handler mapping dictionary routes entity keys to value getters + - **Architecture Benefits**: 42% line reduction in core.py (2,170 → 1,268 lines), clear separation of concerns, improved testability, reusable components + - **See "Common Tasks" section** for detailed patterns and examples +- **Quarter-hour precision**: Entities update on 00/15/30/45-minute boundaries via `schedule_quarter_hour_refresh()` in `coordinator/listeners.py`, not just on data fetch intervals. Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` for absolute-time scheduling. Smart boundary tolerance (±2 seconds) in `sensor/helpers.py` → `round_to_nearest_quarter_hour()` handles HA scheduling jitter: if HA triggers at 14:59:58 → rounds to 15:00:00 (next interval), if HA restarts at 14:59:30 → stays at 14:45:00 (current interval). This ensures current price sensors update without waiting for the next API poll, while preventing premature data display during normal operation. +- **Currency handling**: Multi-currency support with base/sub units (e.g., EUR/ct, NOK/øre) via `get_currency_info()` and `format_price_unit_*()` in `const.py`. +- **Intelligent caching strategy**: Minimizes API calls while ensuring data freshness: + - User data cached for 24h (rarely changes) + - Price data validated against calendar day - cleared on midnight turnover to force fresh fetch + - Cache survives HA restarts via `Store` persistence + - API polling intensifies only when tomorrow's data expected (afternoons) + - Stale cache detection via `_is_cache_valid()` prevents using yesterday's data as today's **Multi-Layer Caching (Performance Optimization)**: @@ -460,6 +456,7 @@ The integration uses **4 distinct caching layers** with automatic invalidation: - **Why**: Avoid expensive calculation (~100-500ms) when data unchanged (70% CPU saving) **Cache Invalidation Coordination**: + - Options change → Explicit `invalidate_config_cache()` on both DataTransformer and PeriodCalculator - Midnight turnover → Clear persistent + transformation cache, period cache auto-invalidates via hash - Tomorrow data arrival → Hash mismatch triggers period recalculation only @@ -542,6 +539,7 @@ custom_components/tibber_prices/ ### Dependency Flow (Calculator Pattern) **Clean Separation:** + ``` sensor/calculators/ → sensor/attributes/ (Volatility only - Hybrid Pattern) sensor/calculators/ → sensor/helpers/ (DailyStat, RollingHour - Pure functions) @@ -553,6 +551,7 @@ sensor/helpers/ ✗ (NO imports from calculators/) ``` **Why this works:** + - **One-way dependencies**: Calculators can import from attributes/helpers, but NOT vice versa - **No circular imports**: Reverse direction is empty (verified Jan 2025) - **Clean testing**: Each layer can be tested independently @@ -562,13 +561,15 @@ sensor/helpers/ ✗ (NO imports from calculators/) **Background:** During Nov 2025 refactoring, Trend and Volatility calculators retained attribute-building logic to avoid duplicating complex calculations. This creates a **backwards dependency** (calculator → attributes) but is INTENTIONAL. **Pattern:** + 1. **Calculator** computes value AND builds attribute dict 2. **Core** stores attributes in `cached_data` dict 3. **Attributes package** retrieves cached attributes via: - - `_add_cached_trend_attributes()` for trend sensors - - `_add_timing_or_volatility_attributes()` for volatility sensors + - `_add_cached_trend_attributes()` for trend sensors + - `_add_timing_or_volatility_attributes()` for volatility sensors **Example (Volatility):** + ```python # sensor/calculators/volatility.py from custom_components.tibber_prices.sensor.attributes import ( @@ -591,6 +592,7 @@ def get_volatility_attributes(self) -> dict | None: ``` **Trade-offs:** + - ✅ **Pro**: Complex logic stays in ONE place (no duplication) - ✅ **Pro**: Calculator has full context for attribute decisions - ❌ **Con**: Violates strict separation (calculator builds attributes) @@ -603,6 +605,7 @@ def get_volatility_attributes(self) -> dict | None: All calculator modules use `TYPE_CHECKING` correctly: **Pattern:** + ```python # Runtime imports (used in function bodies) from custom_components.tibber_prices.const import CONF_PRICE_RATING_THRESHOLD_HIGH @@ -617,23 +620,27 @@ if TYPE_CHECKING: ``` **Rules:** + - ✅ **Runtime imports**: Functions, classes, constants used in code → OUTSIDE TYPE_CHECKING - ✅ **Type-only imports**: Only used in type hints → INSIDE TYPE_CHECKING - ✅ **Coordinator import**: Always in base.py, inherited by all calculators **Verified Status (Jan 2025):** + - All 8 calculators (base, interval, rolling_hour, daily_stat, window_24h, volatility, trend, timing, metadata) use TYPE_CHECKING correctly - No optimization needed - imports are already categorized optimally ### Import Anti-Patterns to Avoid ❌ **DON'T:** + - Import from higher layers (attributes/helpers importing from calculators) - Use runtime imports for type-only dependencies - Create circular dependencies between packages - Import entire modules when only needing one function ✅ **DO:** + - Follow one-way dependency flow (calculators → attributes/helpers) - Use TYPE_CHECKING for type-only imports - Import specific items: `from .helpers import aggregate_price_data` @@ -646,6 +653,7 @@ if TYPE_CHECKING: **Core Challenge:** The period calculation applies **three independent filters** that ALL must pass: + 1. **Flex filter**: `price ≤ daily_min × (1 + flex)` 2. **Min_Distance filter**: `price ≤ daily_avg × (1 - min_distance/100)` 3. **Level filter**: `rating_level IN [allowed_levels]` @@ -655,6 +663,7 @@ The period calculation applies **three independent filters** that ALL must pass: When `daily_min × (1 + flex) > daily_avg × (1 - min_distance/100)`, the flex filter permits intervals that the min_distance filter blocks, causing zero periods despite high flexibility. Example: daily_min=10 ct, daily_avg=20 ct, flex=50%, min_distance=5% + - Flex allows: ≤15 ct - Distance allows: ≤19 ct - But combined: Only intervals ≤15 ct AND ≤19 ct AND matching level → Distance becomes dominant constraint @@ -662,34 +671,36 @@ Example: daily_min=10 ct, daily_avg=20 ct, flex=50%, min_distance=5% **Solutions Implemented (Nov 2025):** 1. **Hard Caps on Flex** (`coordinator/period_handlers/core.py`): - - `MAX_SAFE_FLEX = 0.50` (50% overall maximum) - - `MAX_OUTLIER_FLEX = 0.25` (25% for price spike detection) - - Warns users when base flex exceeds thresholds (INFO at 25%, WARNING at 30%) + - `MAX_SAFE_FLEX = 0.50` (50% overall maximum) + - `MAX_OUTLIER_FLEX = 0.25` (25% for price spike detection) + - Warns users when base flex exceeds thresholds (INFO at 25%, WARNING at 30%) 2. **Relaxation Increment Cap** (`coordinator/period_handlers/relaxation.py`): - - Maximum 3% increment per relaxation step (prevents explosion from high base flex) - - Example: Base flex 40% → increments as 43%, 46%, 49% (capped at 50%) - - Without cap: 40% × 1.25 = 50% step → reaches 100% in 6 steps + - Maximum 3% increment per relaxation step (prevents explosion from high base flex) + - Example: Base flex 40% → increments as 43%, 46%, 49% (capped at 50%) + - Without cap: 40% × 1.25 = 50% step → reaches 100% in 6 steps 3. **Dynamic Min_Distance Scaling** (`coordinator/period_handlers/level_filtering.py`): - - Reduces min_distance proportionally as flex increases above 20% - - Formula: `scale_factor = max(0.25, 1.0 - ((flex - 0.20) × 2.5))` - - Example: flex=30% → scale=0.75 → min_distance reduced by 25% - - Minimum scaling: 25% of original (prevents complete removal) + - Reduces min_distance proportionally as flex increases above 20% + - Formula: `scale_factor = max(0.25, 1.0 - ((flex - 0.20) × 2.5))` + - Example: flex=30% → scale=0.75 → min_distance reduced by 25% + - Minimum scaling: 25% of original (prevents complete removal) 4. **Enhanced Debug Logging** (`coordinator/period_handlers/period_building.py`): - - Tracks exact counts of intervals filtered by flex, min_distance, and level - - Shows which filter blocked the most candidates - - Enables diagnosis of configuration issues + - Tracks exact counts of intervals filtered by flex, min_distance, and level + - Shows which filter blocked the most candidates + - Enables diagnosis of configuration issues **Configuration Guidance:** **Recommended Flex Ranges:** + - **With relaxation enabled**: 10-20% base flex (relaxation will escalate as needed) - **Without relaxation**: 20-35% direct flex (no automatic escalation) - **Anti-pattern**: Base flex >30% with relaxation enabled → causes rapid escalation and filter conflicts **Key Constants** (defined in `coordinator/period_handlers/core.py`): + ```python MAX_SAFE_FLEX = 0.50 # 50% absolute maximum MAX_OUTLIER_FLEX = 0.25 # 25% for stable outlier detection @@ -698,6 +709,7 @@ FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # WARNING at 30% base flex ``` **Relaxation Strategy** (`coordinator/period_handlers/relaxation.py`): + - Per-day independent loops (each day escalates separately based on its needs) - Hard cap: 3% absolute maximum increment per step (prevents explosion from high base flex) - Default configuration: 11 flex levels (15% base → 18% → 21% → ... → 48% max) @@ -705,6 +717,7 @@ FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # WARNING at 30% base flex - Each flex level tries all filter combinations before increasing flex further **Period Boundary Behavior** (`coordinator/period_handlers/period_building.py`): + - Periods can **cross midnight** (day boundaries) naturally - Reference price locked to **period start day** for consistency across the entire period - Pattern: "Uses reference price from start day of the period for consistency" (same as period statistics) @@ -712,6 +725,7 @@ FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # WARNING at 30% base flex - This prevents artificial splits at midnight when prices remain favorable across the boundary **Default Configuration Values** (`const.py`): + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -722,6 +736,7 @@ DEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # 11 steps: 20% → 50% (3% increment The relaxation increment is **hard-coded at 3% per step** in `relaxation.py` for reliability and predictability. This prevents configuration issues with high base flex values while still allowing sufficient escalation to the 50% hard maximum. **Dynamic Scaling Table** (min_distance adjustment): + ``` Flex Scale Example (min_distance=5%) ------------------------------------------- @@ -737,12 +752,14 @@ Flex Scale Example (min_distance=5%) **Testing Scenarios:** When debugging period calculation issues: + 1. Check flex value: Is base flex >30%? Reduce to 15-20% if using relaxation 2. Check logs for "scaled min_distance": Is it reducing too much? May need lower base flex 3. Check filter statistics: Which filter blocks most intervals? (flex, distance, or level) 4. Check relaxation warnings: INFO at 25%, WARNING at 30% indicate suboptimal config **See:** + - **Theory documentation**: `docs/developer/docs/period-calculation-theory.md` (comprehensive mathematical analysis, conflict conditions, configuration pitfalls) - **Implementation**: `coordinator/period_handlers/` package (core.py, relaxation.py, level_filtering.py, period_building.py) - **User guide**: `docs/user/docs/period-calculation.md` (simplified user-facing explanations) @@ -751,14 +768,14 @@ When debugging period calculation issues: **Python Virtual Environment:** -- Project uses `.venv` located at `/home/vscode/.venv/` (outside workspace) -- Symlinked into workspace root as `.venv` → `/home/vscode/.venv/` -- **Why outside workspace?** Project folder is bind-mounted from host, which doesn't support hardlinks required by `uv` +- Project uses `.venv` located at `/home/vscode/.venv/` (outside workspace) +- Symlinked into workspace root as `.venv` → `/home/vscode/.venv/` +- **Why outside workspace?** Project folder is bind-mounted from host, which doesn't support hardlinks required by `uv` **Package Manager:** -- Uses `uv` (modern, fast Python package manager) -- **Always use `uv` commands**, not `pip` directly: +- Uses `uv` (modern, fast Python package manager) +- **Always use `uv` commands**, not `pip` directly: ```bash # ✅ Correct @@ -772,16 +789,16 @@ When debugging period calculation issues: **Development Scripts:** -- All scripts in `./scripts/` automatically use the correct `.venv` -- No need to manually activate venv or specify Python path -- Examples: `./scripts/lint`, `./scripts/develop`, `./scripts/lint-check` -- Release management: `./scripts/release/prepare`, `./scripts/release/generate-notes` +- All scripts in `./scripts/` automatically use the correct `.venv` +- No need to manually activate venv or specify Python path +- Examples: `./scripts/lint`, `./scripts/develop`, `./scripts/lint-check` +- Release management: `./scripts/release/prepare`, `./scripts/release/generate-notes` **Release Note Backends (auto-installed in DevContainer):** -- **Rust toolchain**: Minimal Rust installation via DevContainer feature -- **git-cliff**: Template-based release notes (fast, reliable, installed via cargo in `scripts/setup/setup`) -- Manual grep/awk parsing as fallback (always available) +- **Rust toolchain**: Minimal Rust installation via DevContainer feature +- **git-cliff**: Template-based release notes (fast, reliable, installed via cargo in `scripts/setup/setup`) +- Manual grep/awk parsing as fallback (always available) **Agent Productivity CLI Tools (DevContainer):** @@ -807,9 +824,9 @@ Some commands are available via compatibility aliases because Debian package nam **Debugging Environment Issues:** -- If `import homeassistant` fails: Check if `.venv` symlink exists and points to correct location -- If packages missing: Run `uv sync` to install dependencies from `pyproject.toml` -- If wrong Python version: Verify `.venv/bin/python --version` (should be 3.13+) +- If `import homeassistant` fails: Check if `.venv` symlink exists and points to correct location +- If packages missing: Run `uv sync` to install dependencies from `pyproject.toml` +- If wrong Python version: Verify `.venv/bin/python --version` (should be 3.13+) ## Development Workflow @@ -826,9 +843,9 @@ If you notice commands failing or missing dependencies: **If user insists on local development without container**, warn that: -- You'll need to adapt commands for their local setup -- Some features (like `.venv` symlink) won't work as documented -- Support will be limited (not the intended workflow) +- You'll need to adapt commands for their local setup +- Some features (like `.venv` symlink) won't work as documented +- Support will be limited (not the intended workflow) **Start dev environment:** @@ -866,6 +883,7 @@ _Check-only flow (CI/CD-oriented):_ ``` _Agent behavior rules:_ + - If asked to "fix", "format", "auto-heal", or "make it pass" → start with fix/format scripts. - If asked only to "verify", "validate", or "CI parity" → use check scripts. - After applying fixes, run the relevant check script once to confirm a clean state. @@ -913,7 +931,6 @@ When changes are complete and ready for testing: 1. **Ask user to test**, don't execute `./scripts/develop` yourself 2. **Provide specific test guidance** based on what changed in this session: - - Which UI screens to check (e.g., "Open config flow, step 3") - What behavior to verify (e.g., "Dropdown should show translated values") - What errors to watch for (e.g., "Check logs for JSON parsing errors") @@ -934,19 +951,21 @@ When changes are complete and ready for testing: **What NOT to do:** -- ❌ Don't execute `./scripts/develop` automatically -- ❌ Don't suggest exhaustive testing of unrelated features -- ❌ Don't check `git status` to determine what changed (trust session memory) -- ❌ Don't assume user needs reminding to commit (they manage their own workflow) +- ❌ Don't execute `./scripts/develop` automatically +- ❌ Don't suggest exhaustive testing of unrelated features +- ❌ Don't check `git status` to determine what changed (trust session memory) +- ❌ Don't assume user needs reminding to commit (they manage their own workflow) ## Git Workflow Guidance **Purpose:** Keep commit guidance centralized and avoid duplicated/contradictory rules. **Authoritative commit-message instructions:** + - Use `.github/instructions/commit-messages.instructions.md` for commit type/scope, Impact footer style, and release-notes skip trailers. **Critical behavior rules (still enforced here):** + 1. **Commit execution**: Only run `git commit` when the user explicitly asks to commit. A one-time request to commit does not authorize future commits without asking again. 2. **Commit message generation**: When the user asks only for a commit message, generate the message and stop — do not run `git commit`. The user will commit themselves. 3. **git push**: Never suggest or execute `git push`. The user always handles pushing themselves. @@ -955,10 +974,11 @@ When changes are complete and ready for testing: 6. When suggesting commits, include exact files to stage. **Internal/unreleased fixes:** + - If a fix never affected released users, mark commit body with one trailer so release notes can exclude it: - - `Release-Notes: skip` - - `User-Impact: none` - - `Released-Bug: no` + - `Release-Notes: skip` + - `User-Impact: none` + - `Released-Bug: no` - To check if introducing code was released, use: `./scripts/release/check-if-released ` ### Release Notes Generation @@ -966,21 +986,18 @@ When changes are complete and ready for testing: **Multiple Options Available:** 1. **Helper Script** (recommended, foolproof) - - Script: `./scripts/release/prepare VERSION` - Bumps manifest.json version → commits → creates tag locally - You review and push when ready - Example: `./scripts/release/prepare 0.3.0` 2. **Auto-Tag Workflow** (safety net) - - Workflow: `.github/workflows/auto-tag.yml` - Triggers on manifest.json changes - Automatically creates tag if it doesn't exist - Prevents "forgot to tag" mistakes 3. **Local Script** (testing, preview, and updating releases) - - Script: `./scripts/release/generate-notes [FROM_TAG] [TO_TAG]` - Parses Conventional Commits between tags - Supports multiple backends (auto-detected): @@ -1006,7 +1023,6 @@ When changes are complete and ready for testing: ``` 4. **GitHub UI Button** (manual, PR-based) - - Uses `.github/release.yml` configuration - Click "Generate release notes" when creating release - Works best with PRs that have labels @@ -1070,14 +1086,14 @@ If you want better release notes after the automated release: **Semantic Versioning Rules:** -- **Pre-1.0 (0.x.y)**: - - Breaking changes → bump MINOR (0.x.0) - - New features → bump MINOR (0.x.0) - - Bug fixes → bump PATCH (0.0.x) -- **Post-1.0 (x.y.z)**: - - Breaking changes → bump MAJOR (x.0.0) - - New features → bump MINOR (0.x.0) - - Bug fixes → bump PATCH (0.0.x) +- **Pre-1.0 (0.x.y)**: + - Breaking changes → bump MINOR (0.x.0) + - New features → bump MINOR (0.x.0) + - Bug fixes → bump PATCH (0.0.x) +- **Post-1.0 (x.y.z)**: + - Breaking changes → bump MAJOR (x.0.0) + - New features → bump MINOR (0.x.0) + - Bug fixes → bump PATCH (0.0.x) **Alternative: Manual Bump (with Auto-Tag Safety Net):** @@ -1118,28 +1134,26 @@ USE_AI=false ./scripts/release/generate-notes **Backend Comparison:** -- **GitHub Copilot CLI** (`copilot`): +- **GitHub Copilot CLI** (`copilot`): + - ✅ AI-powered semantic understanding + - ✅ Smart grouping of related commits into single release notes + - ✅ Interprets "Impact:" sections for user-friendly descriptions + - ✅ Multiple commits can be combined with all links: ([hash1](url1), [hash2](url2)) + - ⚠️ Uses premium request quota + - ⚠️ Output may vary between runs - - ✅ AI-powered semantic understanding - - ✅ Smart grouping of related commits into single release notes - - ✅ Interprets "Impact:" sections for user-friendly descriptions - - ✅ Multiple commits can be combined with all links: ([hash1](url1), [hash2](url2)) - - ⚠️ Uses premium request quota - - ⚠️ Output may vary between runs +- **git-cliff** (template-based): + - ✅ Fast and consistent + - ✅ 1:1 commit to release note line mapping + - ✅ Highly configurable via `cliff.toml` + - ❌ No semantic understanding + - ❌ Cannot intelligently group related commits -- **git-cliff** (template-based): - - - ✅ Fast and consistent - - ✅ 1:1 commit to release note line mapping - - ✅ Highly configurable via `cliff.toml` - - ❌ No semantic understanding - - ❌ Cannot intelligently group related commits - -- **manual** (grep/awk): - - ✅ Always available (no dependencies) - - ✅ Basic commit categorization - - ❌ No commit grouping - - ❌ Basic formatting only +- **manual** (grep/awk): + - ✅ Always available (no dependencies) + - ✅ Basic commit categorization + - ❌ No commit grouping + - ❌ Basic formatting only **Output Format:** @@ -1148,17 +1162,17 @@ All backends produce GitHub-flavored Markdown with consistent structure: ```markdown ## 🎉 New Features -- **scope**: Description ([commit_hash](link)) - User-visible impact from "Impact:" section +- **scope**: Description ([commit_hash](link)) + User-visible impact from "Impact:" section -- **scope**: Combined description ([hash1](link1), [hash2](link2)) # AI backend only - Multiple related commits grouped together +- **scope**: Combined description ([hash1](link1), [hash2](link2)) # AI backend only + Multiple related commits grouped together ``` ## 🐛 Bug Fixes -- **scope**: Description ([commit_hash](link)) - User-visible impact +- **scope**: Description ([commit_hash](link)) + User-visible impact ## 📚 Documentation @@ -1177,16 +1191,16 @@ cargo install git-cliff # or download binary from releases **When to Use Which:** -- **GitHub Button**: When working with PRs, quick manual releases -- **Local Script**: Before committing to test release notes, manual review needed -- **CI/CD**: Automatic releases on tag push (production workflow) +- **GitHub Button**: When working with PRs, quick manual releases +- **Local Script**: Before committing to test release notes, manual review needed +- **CI/CD**: Automatic releases on tag push (production workflow) **Format Requirements:** -- **HACS**: No specific format required, uses GitHub releases as-is -- **Home Assistant**: No specific format required for custom integrations -- **Markdown**: Standard GitHub-flavored Markdown supported -- **HTML**: Can include `` tags for special notices (HA update entities only) +- **HACS**: No specific format required, uses GitHub releases as-is +- **Home Assistant**: No specific format required for custom integrations +- **Markdown**: Standard GitHub-flavored Markdown supported +- **HTML**: Can include `` tags for special notices (HA update entities only) **Validate integration:** @@ -1223,6 +1237,7 @@ python -m json.tool custom_components/tibber_prices/translations/de.json > /dev/ This project uses **two complementary tools** with different responsibilities: **Pyright (Type Checker)** - Catches type safety issues: + - ✅ Type mismatches (`str` passed where `int` expected) - ✅ None-safety violations (`Optional[T]` used as `T`) - ✅ Missing/wrong type annotations @@ -1232,6 +1247,7 @@ This project uses **two complementary tools** with different responsibilities: - 🔍 **Always run first** - catches design issues early **Ruff (Linter + Formatter)** - Enforces code style and patterns: + - ✅ Code formatting (line length, indentation, quotes) - ✅ Import ordering (stdlib → third-party → local) - ✅ Unused imports/variables @@ -1358,11 +1374,13 @@ def get_timestamp() -> str: ### Pyright Configuration Project uses `typeCheckingMode = "basic"` in `pyproject.toml`: + - Balanced between strictness and pragmatism - Catches real bugs without excessive noise - Compatible with Home Assistant's typing style **Key settings:** + ```toml [tool.pyright] include = ["custom_components/tibber_prices"] @@ -1374,6 +1392,7 @@ typeCheckingMode = "basic" **CRITICAL: When generating code, always aim for Pyright `basic` mode compliance:** ✅ **DO:** + - Add type hints to all function signatures (parameters + return types) - Use proper type annotations: `dict[str, Any]`, `list[dict]`, `str | None` - Handle Optional types explicitly (None-checks before use) @@ -1381,6 +1400,7 @@ typeCheckingMode = "basic" - Prefer explicit returns over implicit `None` ❌ **DON'T:** + - Leave functions without return type hints - Ignore potential `None` values in Optional types - Use `Any` as escape hatch (only when truly needed) @@ -1399,6 +1419,7 @@ typeCheckingMode = "basic" 3. **Home Assistant API has incomplete typing** **ALWAYS include explanation:** + ```python # ✅ GOOD - Explains why ignore is needed result = tz.localize(dt) # type: ignore[attr-defined] # pytz-specific method @@ -1410,6 +1431,7 @@ result = tz.localize(dt) # type: ignore ### Integration with VS Code Pylance (VS Code's Python language server) uses the same Pyright engine: + - **Red squiggles** = Type errors (must fix) - **Yellow squiggles** = Warnings (should fix) - Hover for details, Cmd/Ctrl+Click for definitions @@ -1430,17 +1452,17 @@ The `./scripts/type-check` script runs the same checks in terminal, ensuring CI/ Calling `ruff` or `uv run ruff` directly can cause unintended side effects: -- May install the integration as a Python package (creates `.egg-info`, etc.) -- HA will then load the **installed** version instead of the **development** version from `custom_components/` -- Causes confusing behavior where code changes don't take effect +- May install the integration as a Python package (creates `.egg-info`, etc.) +- HA will then load the **installed** version instead of the **development** version from `custom_components/` +- Causes confusing behavior where code changes don't take effect **About `__pycache__` directories:** -- **Normal and expected** when Home Assistant runs - this is Python's bytecode cache for faster loading -- **Not a problem** in development - speeds up HA startup -- **Already in `.gitignore`** - won't be committed -- **Only problematic** if the package gets installed in `.venv` (then HA loads installed version, not dev version) -- `./scripts/develop`, `./scripts/lint`, and `./scripts/lint-check` automatically clean up accidental installations +- **Normal and expected** when Home Assistant runs - this is Python's bytecode cache for faster loading +- **Not a problem** in development - speeds up HA startup +- **Already in `.gitignore`** - won't be committed +- **Only problematic** if the package gets installed in `.venv` (then HA loads installed version, not dev version) +- `./scripts/develop`, `./scripts/lint`, and `./scripts/lint-check` automatically clean up accidental installations **Exception:** If you need to run `ruff` with custom flags not supported by our scripts: @@ -1462,20 +1484,20 @@ Calling `ruff` or `uv run ruff` directly can cause unintended side effects: **Note on pip vs. uv pip:** -- `scripts/clean` uses **both** `pip` and `uv pip` for maximum compatibility -- Regular `pip uninstall` has cleaner output (no "Using Python X.Y..." messages) -- `uv pip uninstall` is used as fallback for robustness -- Both are needed because different commands may install via different methods +- `scripts/clean` uses **both** `pip` and `uv pip` for maximum compatibility +- Regular `pip uninstall` has cleaner output (no "Using Python X.Y..." messages) +- `uv pip uninstall` is used as fallback for robustness +- Both are needed because different commands may install via different methods **Ruff Configuration:** -- Max line length: **120** chars (not 88 from Ruff's default) -- Max complexity: **25** (McCabe) -- Target: Python 3.13 -- No unused imports/variables (`F401`, `F841`) -- No mutable default args (`B008`) -- Use `_LOGGER` not `print()` (`T201`) -- `pyproject.toml` (under `[tool.ruff]`) has full configuration +- Max line length: **120** chars (not 88 from Ruff's default) +- Max complexity: **25** (McCabe) +- Target: Python 3.13 +- No unused imports/variables (`F401`, `F841`) +- No mutable default args (`B008`) +- Use `_LOGGER` not `print()` (`T201`) +- `pyproject.toml` (under `[tool.ruff]`) has full configuration ## Critical Project-Specific Patterns @@ -1539,6 +1561,7 @@ When renaming entity keys or changing sensor value units/semantics across releas This is a Home Assistant standard to avoid naming conflicts between integrations and ensure clear ownership of classes. **Naming Pattern:** + ```python # ✅ CORRECT - Integration prefix + semantic purpose class TibberPricesApiClient: # Integration + semantic role @@ -1558,6 +1581,7 @@ class TibberPricesSensorCalculatorTrend: # Too verbose, import path shows loca ``` **IMPORTANT:** Do NOT include package hierarchy in class names. Python's import system provides the namespace: + ```python # The import path IS the full namespace: from custom_components.tibber_prices.coordinator.price_data_manager import TibberPricesPriceDataManager @@ -1569,6 +1593,7 @@ from custom_components.tibber_prices.sensor.calculators.trend import TibberPrice ``` **Home Assistant Core follows this pattern:** + - `TibberDataCoordinator` (not `TibberCoordinatorDataCoordinator`) - `MetWeatherData` (not `MetCoordinatorWeatherData`) - `MetDataUpdateCoordinator` (not `MetCoordinatorDataUpdateCoordinator`) @@ -1576,6 +1601,7 @@ from custom_components.tibber_prices.sensor.calculators.trend import TibberPrice Use semantic prefixes that describe the PURPOSE, not the package location. **When prefix is required:** + - ✅ All public classes (used across multiple modules) - ✅ All exception classes - ✅ All coordinator classes @@ -1584,6 +1610,7 @@ Use semantic prefixes that describe the PURPOSE, not the package location. - ✅ All data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - 🟡 Private helper classes used only within a single module (prefix class name with `_` underscore) - 🟡 Type aliases and callbacks (e.g., `TimeServiceCallback` is acceptable) - 🟡 Small NamedTuples used only for internal function returns (e.g., within calculators) @@ -1593,6 +1620,7 @@ Use semantic prefixes that describe the PURPOSE, not the package location. **Private Classes (Module-Internal):** If you create a helper class that is ONLY used within a single module file: + ```python # ✅ CORRECT - Private class with underscore prefix class _InternalHelper: @@ -1604,11 +1632,13 @@ result = _InternalHelper().process() ``` **When to use private classes:** + - ❌ **DON'T** use for code organization alone - if it deserves a class, it's usually public - ✅ **DO** use for internal implementation details (e.g., state machines, internal builders) - ✅ **DO** use for temporary refactoring helpers (mark as `# TODO: Make public` if it grows) **Example of genuine private class use case:** + ```python # In coordinator/price_data_manager.py class _ApiRetryStateMachine: @@ -1628,21 +1658,21 @@ In practice, most "helper" logic should be **functions**, not classes. Reserve c We use **Ruff** (which replaces Black, Flake8, isort, and more) as our linter and formatter: -- Max line length: **120** chars (not 88 from Ruff's default) -- Max complexity: **25** (McCabe) -- Target: Python 3.13 -- No unused imports/variables (`F401`, `F841`) -- No mutable default args (`B008`) -- Use `_LOGGER` not `print()` (`T201`) +- Max line length: **120** chars (not 88 from Ruff's default) +- Max complexity: **25** (McCabe) +- Target: Python 3.13 +- No unused imports/variables (`F401`, `F841`) +- No mutable default args (`B008`) +- Use `_LOGGER` not `print()` (`T201`) **Pyright config (`pyproject.toml` under `[tool.pyright]`):** We use **Pyright** for static type checking: -- Type checking mode: **basic** (balanced strictness) -- Target: Python 3.13 -- Validates type annotations, None-safety, attribute access -- Integrated with VS Code via Pylance extension +- Type checking mode: **basic** (balanced strictness) +- Target: Python 3.13 +- Validates type annotations, None-safety, attribute access +- Integrated with VS Code via Pylance extension **Import order (enforced by isort):** @@ -1652,19 +1682,19 @@ We use **Pyright** for static type checking: **Import best practices:** -- Prefer Home Assistant utilities over stdlib equivalents: `from homeassistant.util import dt as dt_util` instead of `import datetime` -- Import only specific stdlib types when needed for type hints: `from datetime import date, datetime, timedelta` -- Use `dt_util` for all datetime operations (parsing, timezone conversion, current time) -- Avoid aliasing stdlib modules with same names as HA utilities (e.g., `import datetime as dt` conflicts with `dt_util`) +- Prefer Home Assistant utilities over stdlib equivalents: `from homeassistant.util import dt as dt_util` instead of `import datetime` +- Import only specific stdlib types when needed for type hints: `from datetime import date, datetime, timedelta` +- Use `dt_util` for all datetime operations (parsing, timezone conversion, current time) +- Avoid aliasing stdlib modules with same names as HA utilities (e.g., `import datetime as dt` conflicts with `dt_util`) **Error handling best practices:** -- Keep try blocks minimal - only wrap code that can throw exceptions -- Process data **after** the try/except block, not inside -- Catch specific exceptions, avoid bare `except Exception:` (allowed only in config flows and background tasks) -- Use `ConfigEntryNotReady` for temporary failures (device offline) -- Use `ConfigEntryAuthFailed` for auth issues -- Use `ServiceValidationError` for user input errors in services +- Keep try blocks minimal - only wrap code that can throw exceptions +- Process data **after** the try/except block, not inside +- Catch specific exceptions, avoid bare `except Exception:` (allowed only in config flows and background tasks) +- Use `ConfigEntryNotReady` for temporary failures (device offline) +- Use `ConfigEntryAuthFailed` for auth issues +- Use `ServiceValidationError` for user input errors in services **Logging guidelines:** @@ -1672,34 +1702,32 @@ We use **Pyright** for static type checking: **Why good logging matters beyond debugging:** -- Clear logs become the foundation for good documentation (see "Documentation Writing Strategy") -- If you spend hours making logs explain the logic, that clarity transfers directly to user docs -- Logs show state transitions and decisions that users need to understand -- Pattern: Good hierarchical logs → Easy to extract examples and explanations for documentation +- Clear logs become the foundation for good documentation (see "Documentation Writing Strategy") +- If you spend hours making logs explain the logic, that clarity transfers directly to user docs +- Logs show state transitions and decisions that users need to understand +- Pattern: Good hierarchical logs → Easy to extract examples and explanations for documentation **Log Level Strategy:** -- **INFO Level** - User-facing results and high-level progress: +- **INFO Level** - User-facing results and high-level progress: + - Compact 1-line summaries (no multi-line blocks) + - Important results only (success/failure outcomes) + - No indentation (scannability) + - Example: `"Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0%"` + - Example: `"Day 2025-11-11: Success after 1 relaxation phase (2 periods)"` - - Compact 1-line summaries (no multi-line blocks) - - Important results only (success/failure outcomes) - - No indentation (scannability) - - Example: `"Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0%"` - - Example: `"Day 2025-11-11: Success after 1 relaxation phase (2 periods)"` +- **DEBUG Level** - Detailed execution trace: + - Full context headers with all relevant configuration + - Step-by-step progression through logic + - Hierarchical indentation to show call depth/logic structure + - Intermediate results and calculations + - Example: `" Day 2025-11-11: Found 1 baseline period (need 2)"` + - Example: `" Phase 1: flex 20.25% + original filters"` -- **DEBUG Level** - Detailed execution trace: - - - Full context headers with all relevant configuration - - Step-by-step progression through logic - - Hierarchical indentation to show call depth/logic structure - - Intermediate results and calculations - - Example: `" Day 2025-11-11: Found 1 baseline period (need 2)"` - - Example: `" Phase 1: flex 20.25% + original filters"` - -- **WARNING Level** - Problems and unexpected states: - - Top-level important messages (no indentation) - - Clear indication of what went wrong - - Example: `"Day 2025-11-11: All relaxation phases exhausted, still only 1 period found"` +- **WARNING Level** - Problems and unexpected states: + - Top-level important messages (no indentation) + - Clear indication of what went wrong + - Example: `"Day 2025-11-11: All relaxation phases exhausted, still only 1 period found"` **Hierarchical Indentation Pattern:** @@ -1726,10 +1754,10 @@ _LOGGER.debug("%sExtended baseline period from %s to %s", INDENT_L4, old_end, ne **Why indentation?** -- Makes call stack and decision tree visible at a glance -- Enables quick problem localization (which phase/step failed?) -- Shows parent-child relationships between operations -- Distinguishes between sequential steps vs nested logic +- Makes call stack and decision tree visible at a glance +- Enables quick problem localization (which phase/step failed?) +- Shows parent-child relationships between operations +- Distinguishes between sequential steps vs nested logic **Configuration Context:** @@ -1763,143 +1791,138 @@ for day_date, day_intervals in intervals_by_day.items(): **General Rules:** -- Use lazy logging: `_LOGGER.debug("Message with %s", variable)` (never f-strings in log calls - Ruff G004) -- No periods at end of log messages -- No integration name in messages (added automatically by HA) -- Always include relevant identifiers (day, phase, period) for context -- Log BEFORE and AFTER state changes to show transitions -- Use consistent terminology (e.g., "baseline" vs "relaxed", "extended" vs "replaced") +- Use lazy logging: `_LOGGER.debug("Message with %s", variable)` (never f-strings in log calls - Ruff G004) +- No periods at end of log messages +- No integration name in messages (added automatically by HA) +- Always include relevant identifiers (day, phase, period) for context +- Log BEFORE and AFTER state changes to show transitions +- Use consistent terminology (e.g., "baseline" vs "relaxed", "extended" vs "replaced") **Function organization:** Public entry points → direct helpers (call order) → pure utilities. Prefix private helpers with `_`. **Legacy/Backwards compatibility:** -- **Do NOT add legacy migration code** unless the change was already released in a version tag -- **Check if released**: Use `./scripts/release/check-if-released ` to verify if code is in any `v*.*.*` tag -- **Example**: If introducing breaking config change in commit `abc123`, run `./scripts/release/check-if-released abc123`: - - ✓ NOT RELEASED → No migration needed, just use new code - - ✗ ALREADY RELEASED → Migration may be needed for users upgrading from that version -- **Rule**: Only add backwards compatibility for changes that shipped to users via HACS/GitHub releases -- **Prefer breaking changes over complexity**: If migration code would be complex or clutter the codebase, prefer documenting the breaking change in release notes (Home Assistant style). Only add simple migrations (e.g., `.lower()` call, key rename) when trivial. +- **Do NOT add legacy migration code** unless the change was already released in a version tag +- **Check if released**: Use `./scripts/release/check-if-released ` to verify if code is in any `v*.*.*` tag +- **Example**: If introducing breaking config change in commit `abc123`, run `./scripts/release/check-if-released abc123`: + - ✓ NOT RELEASED → No migration needed, just use new code + - ✗ ALREADY RELEASED → Migration may be needed for users upgrading from that version +- **Rule**: Only add backwards compatibility for changes that shipped to users via HACS/GitHub releases +- **Prefer breaking changes over complexity**: If migration code would be complex or clutter the codebase, prefer documenting the breaking change in release notes (Home Assistant style). Only add simple migrations (e.g., `.lower()` call, key rename) when trivial. **Translation sync:** When updating `/translations/en.json`, update ALL language files (`de.json`, etc.) with same keys (placeholder values OK). **Documentation language:** -- **CRITICAL**: All user-facing documentation (`README.md`, `docs/user/docs/`, `docs/developer/docs/`) MUST be written in **English** -- **Code comments**: Always use English for code comments and docstrings -- **UI translations**: Multi-language support exists in `/translations/` and `/custom_translations/` (de, en, nb, nl, sv) for UI strings only -- **Why English-only docs**: Ensures maintainability, accessibility to global community, and consistency with Home Assistant ecosystem -- **Entity names in documentation**: Use **translated display names** from `/translations/en.json` (what users see), not internal entity IDs. Example: "Best Price Period" not "sensor.tibber_home_best_price_period" (add entity ID as comment if needed for clarity). -- **Entity reference annotations**: When mentioning entity display names in docs, add the `translation_key` (= entity ID suffix) on first mention per section: `**Display Name** (\`translation_key\`)`. This helps users find entities regardless of UI language. See `docs/user/docs/sensor-reference.md` for the auto-generated multi-language lookup table. -- **Entity ID tip boxes**: All doc pages with entity ID examples should include the standardized tip box linking to `sensor-reference.md`. Use the same wording as in existing pages (search for "Entity ID tip" for the template). +- **CRITICAL**: All user-facing documentation (`README.md`, `docs/user/docs/`, `docs/developer/docs/`) MUST be written in **English** +- **Code comments**: Always use English for code comments and docstrings +- **UI translations**: Multi-language support exists in `/translations/` and `/custom_translations/` (de, en, nb, nl, sv) for UI strings only +- **Why English-only docs**: Ensures maintainability, accessibility to global community, and consistency with Home Assistant ecosystem +- **Entity names in documentation**: Use **translated display names** from `/translations/en.json` (what users see), not internal entity IDs. Example: "Best Price Period" not "sensor.tibber_home_best_price_period" (add entity ID as comment if needed for clarity). +- **Entity reference annotations**: When mentioning entity display names in docs, add the `translation_key` (= entity ID suffix) on first mention per section: `**Display Name** (\`translation_key\`)`. This helps users find entities regardless of UI language. See `docs/user/docs/sensor-reference.md` for the auto-generated multi-language lookup table. +- **Entity ID tip boxes**: All doc pages with entity ID examples should include the standardized tip box linking to `sensor-reference.md`. Use the same wording as in existing pages (search for "Entity ID tip" for the template). **Examples and use cases:** -- **Regional context**: Tibber operates primarily in European markets (Norway, Sweden, Germany, Netherlands). Examples should reflect European context: - - ✅ Use cases: Heat pump, dishwasher, washing machine, electric vehicle charging, water heater - - ✅ Appliances: Common in European homes (heat pumps for heating/cooling, instantaneous water heaters) - - ✅ Energy patterns: European pricing structures (often lower overnight rates, higher daytime rates) - - ✅ Optimization strategies: ECO programs with long run times, heat pump defrost cycles, smart water heating - - ❌ Avoid: US-centric examples (central air conditioning as primary cooling, 240V dryers, different voltage standards) - - ❌ Avoid: US appliance behavior assumptions (e.g., dishwashers requiring hot water connection due to 120V limitations) -- **Technical differences**: European appliances operate differently due to 230V power supply: - - Dishwashers: Built-in heaters, ECO programs (long duration, low energy), cold water connection standard - - Washing machines: Fast heating cycles, higher temperature options (60°C, 90°C programs common) - - Heat pumps: Primary heating source (not just cooling), complex defrost cycles, weather-dependent operation -- **Units and formats**: Use European conventions where appropriate: - - Prices: ct/kWh or øre/kWh (as provided by Tibber API) - - Time: 24-hour format (00:00-23:59) - - Dates: ISO 8601 format (YYYY-MM-DD) +- **Regional context**: Tibber operates primarily in European markets (Norway, Sweden, Germany, Netherlands). Examples should reflect European context: + - ✅ Use cases: Heat pump, dishwasher, washing machine, electric vehicle charging, water heater + - ✅ Appliances: Common in European homes (heat pumps for heating/cooling, instantaneous water heaters) + - ✅ Energy patterns: European pricing structures (often lower overnight rates, higher daytime rates) + - ✅ Optimization strategies: ECO programs with long run times, heat pump defrost cycles, smart water heating + - ❌ Avoid: US-centric examples (central air conditioning as primary cooling, 240V dryers, different voltage standards) + - ❌ Avoid: US appliance behavior assumptions (e.g., dishwashers requiring hot water connection due to 120V limitations) +- **Technical differences**: European appliances operate differently due to 230V power supply: + - Dishwashers: Built-in heaters, ECO programs (long duration, low energy), cold water connection standard + - Washing machines: Fast heating cycles, higher temperature options (60°C, 90°C programs common) + - Heat pumps: Primary heating source (not just cooling), complex defrost cycles, weather-dependent operation +- **Units and formats**: Use European conventions where appropriate: + - Prices: ct/kWh or øre/kWh (as provided by Tibber API) + - Time: 24-hour format (00:00-23:59) + - Dates: ISO 8601 format (YYYY-MM-DD) **Language style and tone:** -- **Informal address**: Always use informal "you" forms (German: "du" not "Sie", Dutch: "je/jouw" not "u/uw"). This applies to all translations. -- **Gender-neutral language**: Use gender-neutral formulations where possible, but keep them natural - avoid forced or artificial constructions. -- **Documentation tone**: English documentation should use a friendly, approachable tone. Avoid overly formal constructions like "It is recommended that you..." - prefer "We recommend..." or "You can...". -- **Imperative mood**: Use direct imperatives for instructions: "Configure the integration" not "You should configure the integration". -- **Language-specific notes**: - - German: Use "du" (informal) and gender-neutral imperatives (e.g., "Konfiguriere" instead of "Konfigurieren Sie") - - Dutch: Use "je/jouw" (informal) instead of "u/uw" (formal) - - Swedish/Norwegian: Already use informal address by default (no formal "Ni"/"De" in modern usage) - - English: Already gender-neutral and appropriately informal +- **Informal address**: Always use informal "you" forms (German: "du" not "Sie", Dutch: "je/jouw" not "u/uw"). This applies to all translations. +- **Gender-neutral language**: Use gender-neutral formulations where possible, but keep them natural - avoid forced or artificial constructions. +- **Documentation tone**: English documentation should use a friendly, approachable tone. Avoid overly formal constructions like "It is recommended that you..." - prefer "We recommend..." or "You can...". +- **Imperative mood**: Use direct imperatives for instructions: "Configure the integration" not "You should configure the integration". +- **Language-specific notes**: + - German: Use "du" (informal) and gender-neutral imperatives (e.g., "Konfiguriere" instead of "Konfigurieren Sie") + - Dutch: Use "je/jouw" (informal) instead of "u/uw" (formal) + - Swedish/Norwegian: Already use informal address by default (no formal "Ni"/"De" in modern usage) + - English: Already gender-neutral and appropriately informal **User Documentation Quality:** When writing or updating user-facing documentation (`docs/user/docs/` or `docs/developer/docs/`), follow these principles learned from real user feedback: -- **Clarity over completeness**: Users want to understand concepts, not read technical specifications - - ✅ Good: "Relaxation automatically loosens filters until enough periods are found" - - ❌ Bad: "The relaxation algorithm implements a 4×4 matrix strategy with multiplicative flex increments" -- **Visual examples**: Use timeline diagrams, code blocks with comments, before/after comparisons - - ✅ Show what a "period" looks like on a 24-hour timeline - - ✅ Include automation examples with real entity names -- **Use-case driven**: Start with "what can I do with this?" not "how does it work internally" - - ✅ Structure: Quick Start → Common Scenarios → Configuration Guide → Advanced Topics - - ❌ Avoid: Starting with mathematical formulas or algorithm descriptions -- **Practical troubleshooting**: Address real problems users encounter - - ✅ "No periods found → Try: increase flex from 15% to 20%" - - ❌ Avoid: Generic "check your configuration" without specific guidance -- **Progressive disclosure**: Basic concepts first, advanced details later - - ✅ Main doc covers 80% use cases in simple terms - - ✅ Link to advanced/technical docs for edge cases - - ❌ Don't mix basic explanations with deep technical details -- **When code changed significantly**: Verify documentation still matches - - If relaxation strategy changed from 3 phases to 4×4 matrix → documentation MUST reflect this - - If metadata format changed → update all examples showing attributes - - If per-day independence was added → explain why some days relax differently +- **Clarity over completeness**: Users want to understand concepts, not read technical specifications + - ✅ Good: "Relaxation automatically loosens filters until enough periods are found" + - ❌ Bad: "The relaxation algorithm implements a 4×4 matrix strategy with multiplicative flex increments" +- **Visual examples**: Use timeline diagrams, code blocks with comments, before/after comparisons + - ✅ Show what a "period" looks like on a 24-hour timeline + - ✅ Include automation examples with real entity names +- **Use-case driven**: Start with "what can I do with this?" not "how does it work internally" + - ✅ Structure: Quick Start → Common Scenarios → Configuration Guide → Advanced Topics + - ❌ Avoid: Starting with mathematical formulas or algorithm descriptions +- **Practical troubleshooting**: Address real problems users encounter + - ✅ "No periods found → Try: increase flex from 15% to 20%" + - ❌ Avoid: Generic "check your configuration" without specific guidance +- **Progressive disclosure**: Basic concepts first, advanced details later + - ✅ Main doc covers 80% use cases in simple terms + - ✅ Link to advanced/technical docs for edge cases + - ❌ Don't mix basic explanations with deep technical details +- **When code changed significantly**: Verify documentation still matches + - If relaxation strategy changed from 3 phases to 4×4 matrix → documentation MUST reflect this + - If metadata format changed → update all examples showing attributes + - If per-day independence was added → explain why some days relax differently **Documentation Writing Strategy:** Understanding **how** good documentation emerges is as important as knowing what makes it good: -- **Live Understanding vs. Code Analysis** +- **Live Understanding vs. Code Analysis** + - ✅ **DO:** Write docs during/after active development + - When implementing complex logic, document it while the "why" is fresh + - Use real examples from debugging sessions (actual logs, real data) + - Document decisions as they're made, not after the fact + - ❌ **DON'T:** Write docs from cold code analysis + - Reading code shows "what", not "why" + - Missing context: Which alternatives were considered? + - No user perspective: What's actually confusing? - - ✅ **DO:** Write docs during/after active development - - When implementing complex logic, document it while the "why" is fresh - - Use real examples from debugging sessions (actual logs, real data) - - Document decisions as they're made, not after the fact - - ❌ **DON'T:** Write docs from cold code analysis - - Reading code shows "what", not "why" - - Missing context: Which alternatives were considered? - - No user perspective: What's actually confusing? - -- **User Feedback Loop** - - - Key insight: Documentation improves when users question it - - Pattern: +- **User Feedback Loop** + - Key insight: Documentation improves when users question it + - Pattern: 1. User asks: "Does this still match the code?" 2. AI realizes: "Oh, the 3-phase model is outdated" 3. Together we trace through real behavior 4. Documentation gets rewritten with correct mental model - - Why it works: User questions force critical thinking, real confusion points get addressed + - Why it works: User questions force critical thinking, real confusion points get addressed -- **Log-Driven Documentation** +- **Log-Driven Documentation** + - Observation: When logs explain logic clearly, documentation becomes easier + - Why: Logs show state transitions ("Baseline insufficient → Starting relaxation"), decisions ("Replaced period X with larger Y"), and are already written for humans + - Pattern: If you spent hours making logs clear → use that clarity in documentation too - - Observation: When logs explain logic clearly, documentation becomes easier - - Why: Logs show state transitions ("Baseline insufficient → Starting relaxation"), decisions ("Replaced period X with larger Y"), and are already written for humans - - Pattern: If you spent hours making logs clear → use that clarity in documentation too +- **Concrete Examples > Abstract Descriptions** + - ✅ **Good:** "Day 2025-11-11 found 2 periods at flex=12.0% +volatility_any (stopped early, no need to try higher flex)" + - ❌ **Bad:** "The relaxation algorithm uses a configurable threshold multiplier with filter combination strategies" + - Use real data from debug sessions, show actual attribute values, demonstrate with timeline diagrams -- **Concrete Examples > Abstract Descriptions** +- **Context Accumulation in Long Sessions** + - Advantage: AI builds mental model incrementally, sees evolution of logic (not just final state), understands trade-offs + - Disadvantage of short sessions: Cold start every time, missing "why" context, documentation becomes spec-writing + - Lesson: Complex documentation benefits from focused, uninterrupted work with accumulated context - - ✅ **Good:** "Day 2025-11-11 found 2 periods at flex=12.0% +volatility_any (stopped early, no need to try higher flex)" - - ❌ **Bad:** "The relaxation algorithm uses a configurable threshold multiplier with filter combination strategies" - - Use real data from debug sessions, show actual attribute values, demonstrate with timeline diagrams - -- **Context Accumulation in Long Sessions** - - - Advantage: AI builds mental model incrementally, sees evolution of logic (not just final state), understands trade-offs - - Disadvantage of short sessions: Cold start every time, missing "why" context, documentation becomes spec-writing - - Lesson: Complex documentation benefits from focused, uninterrupted work with accumulated context - -- **Document the "Why", Not Just the "What"** - - Every complex pattern should answer: +- **Document the "Why", Not Just the "What"** + - Every complex pattern should answer: 1. **What** does it do? (quick summary) 2. **Why** was it designed this way? (alternatives considered) 3. **How** does a user benefit? (practical impact) 4. **When** does it fail? (known limitations) - - Example: "Replacement Logic: Larger periods replace smaller overlapping ones because users want ONE long cheap period, not multiple short overlapping ones." + - Example: "Replacement Logic: Larger periods replace smaller overlapping ones because users want ONE long cheap period, not multiple short overlapping ones." ## Ruff Code Style Guidelines @@ -2079,11 +2102,11 @@ result = [ # Use regular loop instead **Common Ruff Auto-fixes:** -- Unused imports → removed automatically -- Unused variables → prefixed with `_` if intentional: `_unused = value` -- Mutable default args → use `None` with `if x is None: x = []` -- `== True` / `== False` → simplified to `if x:` / `if not x:` -- Long lines → Ruff suggests breaks but may need manual adjustment +- Unused imports → removed automatically +- Unused variables → prefixed with `_` if intentional: `_unused = value` +- Mutable default args → use `None` with `if x is None: x = []` +- `== True` / `== False` → simplified to `if x:` / `if not x:` +- Long lines → Ruff suggests breaks but may need manual adjustment ## Attribute Naming Conventions @@ -2150,8 +2173,8 @@ The `timestamp` attribute **MUST always be first** in every sensor's attributes. All sensors (both `sensor` and `binary_sensor` platforms) automatically receive a default `timestamp` attribute set to the **current time rounded to the nearest quarter hour** (00, 15, 30, or 45 minutes). This is handled using unified attribute builder functions: -- **Sensor platform**: `sensor/attributes.py` → `build_extra_state_attributes()` (called from `sensor/core.py` → `extra_state_attributes` property) -- **Binary sensor platform**: `binary_sensor/attributes.py` → `build_async_extra_state_attributes()` and `build_sync_extra_state_attributes()` (called from `binary_sensor/core.py` properties) +- **Sensor platform**: `sensor/attributes.py` → `build_extra_state_attributes()` (called from `sensor/core.py` → `extra_state_attributes` property) +- **Binary sensor platform**: `binary_sensor/attributes.py` → `build_async_extra_state_attributes()` and `build_sync_extra_state_attributes()` (called from `binary_sensor/core.py` properties) Both platforms use the same pattern: a `build_*_extra_state_attributes()` function that generates the default timestamp, merges sensor-specific attributes, and ensures timestamp ordering. @@ -2161,19 +2184,19 @@ The rounding uses `round_to_nearest_quarter_hour()` from `average_utils.py`, whi Individual sensors can override the default timestamp to reflect different time contexts: -- **Current interval sensors**: Use default (rounded quarter) - represents when calculation was made -- **Next interval sensors**: Override with next interval's `startsAt` - shows when that interval starts -- **Previous interval sensors**: Override with previous interval's `startsAt` - shows when that interval started -- **Statistical sensors (min/max)**: Override with extreme interval's `startsAt` - shows when the extreme price occurs -- **Daily average sensors**: Override with midnight (00:00) of that day - shows the value applies to the whole day -- **Daily aggregated sensors**: Override with midnight (00:00) of that day - shows the value applies to the whole day -- **Daily volatility sensors**: Override with day start (00:00 of yesterday/today/tomorrow) - shows which day's data is analyzed -- **Next 24h volatility sensor**: Override with current time (not rounded) - shows the exact start of the 24h window -- **Future forecast sensors**: Override with first interval's `startsAt` - shows when the forecast window begins -- **Timing sensors** (`best_price_end_time`, etc.): Override with minute-precise or quarter-rounded time - shows current calculation time with appropriate precision -- **Period sensors**: Use default (rounded quarter) - represents when period state was determined (via binary_sensor attribute functions) -- **Chart data export**: Overrides with service call timestamp (when data was requested) -- **Data timestamp sensor**: Overrides with API's data timestamp (when data was fetched from Tibber) +- **Current interval sensors**: Use default (rounded quarter) - represents when calculation was made +- **Next interval sensors**: Override with next interval's `startsAt` - shows when that interval starts +- **Previous interval sensors**: Override with previous interval's `startsAt` - shows when that interval started +- **Statistical sensors (min/max)**: Override with extreme interval's `startsAt` - shows when the extreme price occurs +- **Daily average sensors**: Override with midnight (00:00) of that day - shows the value applies to the whole day +- **Daily aggregated sensors**: Override with midnight (00:00) of that day - shows the value applies to the whole day +- **Daily volatility sensors**: Override with day start (00:00 of yesterday/today/tomorrow) - shows which day's data is analyzed +- **Next 24h volatility sensor**: Override with current time (not rounded) - shows the exact start of the 24h window +- **Future forecast sensors**: Override with first interval's `startsAt` - shows when the forecast window begins +- **Timing sensors** (`best_price_end_time`, etc.): Override with minute-precise or quarter-rounded time - shows current calculation time with appropriate precision +- **Period sensors**: Use default (rounded quarter) - represents when period state was determined (via binary_sensor attribute functions) +- **Chart data export**: Overrides with service call timestamp (when data was requested) +- **Data timestamp sensor**: Overrides with API's data timestamp (when data was fetched from Tibber) **Key Principles:** The timestamp represents one of these concepts: @@ -2214,6 +2237,7 @@ def _get_sensor_attributes(self) -> dict | None: ``` **Why direct method over Callable pattern?** + - **Simpler**: No lambda/Callable indirection, clearer stack traces - **More HA-standard**: Most Core integrations use direct methods - **Better performance**: ~2x faster (~0.1-0.5μs vs 0.2-0.8μs per call) @@ -2225,6 +2249,7 @@ def _get_sensor_attributes(self) -> dict | None: Both platforms now use **identical signatures and patterns** (unified Nov 2025): **Sensor Platform (`sensor/attributes.py`):** + ```python def build_extra_state_attributes( entity_key: str, @@ -2243,6 +2268,7 @@ def build_extra_state_attributes( ``` **Binary Sensor Platform (`binary_sensor/attributes.py`):** + ```python async def build_async_extra_state_attributes( entity_key: str, @@ -2262,6 +2288,7 @@ def build_sync_extra_state_attributes(...) -> dict | None: ``` **Key Points:** + - **Architectural consistency**: Both platforms use direct method pattern (not Callable) - **Naming consistency**: Both use `_get_sensor_attributes()` method name - **Parameter consistency**: Both builders accept `sensor_attrs` parameter @@ -2299,13 +2326,13 @@ This ensures timestamp is always the first key in the attribute dict, regardless **Rationale:** -- **Time first**: Users need to know when/for which interval the data applies before interpreting values -- **Decisions next**: Core attributes for automation logic (is it cheap/expensive?) -- **Prices after**: Actual values to display or use in calculations -- **Differences optionally**: Contextual comparisons if relevant -- **Details follow**: Supplementary information for deeper analysis -- **Meta last**: Complex nested data and technical information -- **Descriptions always last**: Human-readable help text from `custom_translations/` (must always be defined; `description` always shown, `long_description` and `usage_tips` shown only when user enables `CONF_EXTENDED_DESCRIPTIONS`) +- **Time first**: Users need to know when/for which interval the data applies before interpreting values +- **Decisions next**: Core attributes for automation logic (is it cheap/expensive?) +- **Prices after**: Actual values to display or use in calculations +- **Differences optionally**: Contextual comparisons if relevant +- **Details follow**: Supplementary information for deeper analysis +- **Meta last**: Complex nested data and technical information +- **Descriptions always last**: Human-readable help text from `custom_translations/` (must always be defined; `description` always shown, `long_description` and `usage_tips` shown only when user enables `CONF_EXTENDED_DESCRIPTIONS`) **In Practice:** @@ -2337,45 +2364,45 @@ This ensures timestamp is always the first key in the attribute dict, regardless **Time-based Attributes:** -- Use `next_*` for future calculations starting from the next interval (not "future\_\*") -- Use `trailing_*` for backward-looking calculations -- Use `leading_*` for forward-looking calculations -- Always include the time span: `next_3h_avg`, `trailing_24h_max` -- For multi-part periods, be specific: `second_half_6h_avg` (not "later_half") +- Use `next_*` for future calculations starting from the next interval (not "future\_\*") +- Use `trailing_*` for backward-looking calculations +- Use `leading_*` for forward-looking calculations +- Always include the time span: `next_3h_avg`, `trailing_24h_max` +- For multi-part periods, be specific: `second_half_6h_avg` (not "later_half") **Counting Attributes:** -- Use singular `_count` for counting items: `interval_count`, `period_count` -- Exception: `intervals_available` is a status indicator (how many are available), not a count of items being processed -- Prefer singular form: `interval_count` over `intervals_count` (the word "count" already implies plurality) +- Use singular `_count` for counting items: `interval_count`, `period_count` +- Exception: `intervals_available` is a status indicator (how many are available), not a count of items being processed +- Prefer singular form: `interval_count` over `intervals_count` (the word "count" already implies plurality) **Difference/Comparison Attributes:** -- Use `_diff` suffix (not "difference") -- Always specify what is being compared: `price_diff_from_daily_min`, `second_half_3h_diff_from_current` -- For percentages, use `_diff_%` suffix with underscore: `price_diff_from_max_%` +- Use `_diff` suffix (not "difference") +- Always specify what is being compared: `price_diff_from_daily_min`, `second_half_3h_diff_from_current` +- For percentages, use `_diff_%` suffix with underscore: `price_diff_from_max_%` **Duration Attributes:** -- Be specific about scope: `remaining_minutes_in_period` (not "after_interval") -- Pattern: `{remaining/elapsed}_{unit}_in_{scope}` +- Be specific about scope: `remaining_minutes_in_period` (not "after_interval") +- Pattern: `{remaining/elapsed}_{unit}_in_{scope}` **Status/Boolean Attributes:** -- Use descriptive suffixes: `data_available` (not just "available") -- Qualify generic terms: `data_status` (not just "status") -- Pattern: `{what}_{status_type}` like `tomorrow_data_status` +- Use descriptive suffixes: `data_available` (not just "available") +- Qualify generic terms: `data_status` (not just "status") +- Pattern: `{what}_{status_type}` like `tomorrow_data_status` **Grouped/Nested Data:** -- Describe the grouping: `intervals_by_hour` (not just "hours") -- Pattern: `{items}_{grouping_method}` +- Describe the grouping: `intervals_by_hour` (not just "hours") +- Pattern: `{items}_{grouping_method}` **Price-Related Attributes:** -- Period statistics: `price_mean` (arithmetic mean), `price_median` (median value) -- Reference comparisons: `period_price_diff_from_daily_min` (period mean vs daily min) -- Interval-specific: `interval_price_diff_from_daily_max` (current interval vs daily max) +- Period statistics: `price_mean` (arithmetic mean), `price_median` (median value) +- Reference comparisons: `period_price_diff_from_daily_min` (period mean vs daily min) +- Interval-specific: `interval_price_diff_from_daily_max` (current interval vs daily max) ### Before Adding New Attributes @@ -2395,7 +2422,6 @@ If the answer to any is "no", make the name more explicit. After the sensor.py refactoring (completed Nov 2025), sensors are organized by **calculation method** rather than feature type. Follow these steps: 1. **Determine calculation pattern** - Choose which group your sensor belongs to: - - **Interval-based**: Uses time offset from current interval (e.g., current/next/previous) - **Rolling hour**: Aggregates 5-interval window (2 before + center + 2 after) - **Daily statistics**: Min/max/avg within calendar day boundaries @@ -2407,7 +2433,6 @@ After the sensor.py refactoring (completed Nov 2025), sensors are organized by * **IMPORTANT — After adding/renaming entities**: Run `./scripts/docs/generate-sensor-reference` to regenerate the multi-language sensor reference table. The `scripts/check` and CI will fail if the reference is stale. 2. **Add entity description** to appropriate sensor group in `sensor/definitions.py`: - - `INTERVAL_PRICE_SENSORS`, `INTERVAL_LEVEL_SENSORS`, or `INTERVAL_RATING_SENSORS` - `ROLLING_HOUR_PRICE_SENSORS`, `ROLLING_HOUR_LEVEL_SENSORS`, or `ROLLING_HOUR_RATING_SENSORS` - `DAILY_STAT_SENSORS` @@ -2417,7 +2442,6 @@ After the sensor.py refactoring (completed Nov 2025), sensors are organized by * - `DIAGNOSTIC_SENSORS` 3. **Add handler mapping** in `sensor/core.py` → `_get_value_getter()` method: - - For interval-based: Use `_get_interval_value(interval_offset, value_type)` - For rolling hour: Use `_get_rolling_hour_value(hour_offset, value_type)` - For daily stats: Use `_get_daily_stat_value(day, stat_func)` @@ -2436,28 +2460,25 @@ After the sensor.py refactoring (completed Nov 2025), sensors are organized by * The refactoring consolidated duplicate logic into unified methods in `sensor/core.py`: -- **`_get_interval_value(interval_offset, value_type, in_euro=False)`** +- **`_get_interval_value(interval_offset, value_type, in_euro=False)`** + - Replaces: `_get_interval_price_value()`, `_get_interval_level_value()`, `_get_interval_rating_value()` + - Handles: All interval-based sensors (current/next/previous) + - Returns: Price (float), level (str), or rating (str) based on value_type - - Replaces: `_get_interval_price_value()`, `_get_interval_level_value()`, `_get_interval_rating_value()` - - Handles: All interval-based sensors (current/next/previous) - - Returns: Price (float), level (str), or rating (str) based on value_type +- **`_get_rolling_hour_value(hour_offset, value_type)`** + - Replaces: `_get_rolling_hour_average_value()`, `_get_rolling_hour_level_value()`, `_get_rolling_hour_rating_value()` + - Handles: All 5-interval rolling hour windows + - Returns: Aggregated value (average price, aggregated level/rating) -- **`_get_rolling_hour_value(hour_offset, value_type)`** +- **`_get_daily_stat_value(day, stat_func)`** + - Replaces: `_get_statistics_value()` (calendar day portion) + - Handles: Min/max/avg for calendar days (today/tomorrow) + - Returns: Price in subunit currency units (cents/øre) - - Replaces: `_get_rolling_hour_average_value()`, `_get_rolling_hour_level_value()`, `_get_rolling_hour_rating_value()` - - Handles: All 5-interval rolling hour windows - - Returns: Aggregated value (average price, aggregated level/rating) - -- **`_get_daily_stat_value(day, stat_func)`** - - - Replaces: `_get_statistics_value()` (calendar day portion) - - Handles: Min/max/avg for calendar days (today/tomorrow) - - Returns: Price in subunit currency units (cents/øre) - -- **`_get_24h_window_value(stat_func)`** - - Replaces: `_get_average_value()`, `_get_minmax_value()` - - Handles: Trailing/leading 24h window statistics - - Returns: Price in subunit currency units (cents/øre) +- **`_get_24h_window_value(stat_func)`** + - Replaces: `_get_average_value()`, `_get_minmax_value()` + - Handles: Trailing/leading 24h window statistics + - Returns: Price in subunit currency units (cents/øre) Legacy wrapper methods still exist for backward compatibility but will be removed in a future cleanup phase. @@ -2481,13 +2502,11 @@ Edit `utils/price.py` or `utils/average.py`. These are stateless pure functions The config flow is split into three separate flow handlers: 1. **User Flow** (`config_flow/user_flow.py`) - Initial setup and reauth - - `async_step_user()` - API token input - `async_step_select_home()` - Home selection - `async_step_reauth()` / `async_step_reauth_confirm()` - Reauth flow 2. **Subentry Flow** (`config_flow/subentry_flow.py`) - Add additional homes - - `async_step_user()` - Select from available homes - `async_step_init()` - Subentry options @@ -2526,34 +2545,34 @@ When encountering unfamiliar HA patterns (especially UI/config flow/translation **1. Check Official HA Documentation First:** -- **Config Flow**: https://developers.home-assistant.io/docs/config_entries_config_flow_handler -- **Translations**: https://developers.home-assistant.io/docs/internationalization/core -- **Selectors**: https://developers.home-assistant.io/docs/blueprint/selectors -- **Data Entry Flow**: https://developers.home-assistant.io/docs/data_entry_flow_index +- **Config Flow**: https://developers.home-assistant.io/docs/config_entries_config_flow_handler +- **Translations**: https://developers.home-assistant.io/docs/internationalization/core +- **Selectors**: https://developers.home-assistant.io/docs/blueprint/selectors +- **Data Entry Flow**: https://developers.home-assistant.io/docs/data_entry_flow_index **2. Search HA Core Codebase:** -- Repository: https://github.com/home-assistant/core -- Look for similar patterns in core integrations (use GitHub search) -- Check `homeassistant/helpers/` for utility patterns -- Example: Search for `translation_key` usage to see real-world examples +- Repository: https://github.com/home-assistant/core +- Look for similar patterns in core integrations (use GitHub search) +- Check `homeassistant/helpers/` for utility patterns +- Example: Search for `translation_key` usage to see real-world examples **3. Test Incrementally:** -- Make small changes, test each one -- Don't assume complex solutions work without verification -- Ask user to test with `./scripts/develop` when needed +- Make small changes, test each one +- Don't assume complex solutions work without verification +- Ask user to test with `./scripts/develop` when needed **Real Example from This Project:** During translation implementation, we tried several incorrect structures: -- ❌ `selector.select.options.{field_name}` (didn't work) -- ❌ `selector.select.{translation_key}` (didn't work) -- ❌ `options.step.{step_id}.data.{field}.options` (overly complex) +- ❌ `selector.select.options.{field_name}` (didn't work) +- ❌ `selector.select.{translation_key}` (didn't work) +- ❌ `options.step.{step_id}.data.{field}.options` (overly complex) Only after consulting the official HA docs did we discover the correct pattern: -- ✅ `selector.{translation_key}.options.{value}` (simple, flat structure) +- ✅ `selector.{translation_key}.options.{value}` (simple, flat structure) **Lesson:** When stuck, consult official docs first - don't guess at complex nested structures. @@ -2561,29 +2580,32 @@ Only after consulting the official HA docs did we discover the correct pattern: **Never do these:** -- ❌ **Blocking operations in event loop**: Use `aiohttp` with `async_get_clientsession(hass)`, not `requests.get()`. Use `await asyncio.sleep()`, not `time.sleep()`. -- ❌ **Processing data inside try block**: Move data processing outside exception handlers. Only API calls belong in try blocks. -- ❌ **Hardcoded strings (not translatable)**: Use `translation_key` instead of `_attr_name = "Temperature Sensor"`. -- ❌ **Accessing hass.data directly in tests**: Use proper fixtures. -- ❌ **User-configurable polling intervals**: Integration determines this, not users. -- ❌ **Using standard library datetime**: Use `dt_util.now()` instead of `datetime.now()`. +- ❌ **Blocking operations in event loop**: Use `aiohttp` with `async_get_clientsession(hass)`, not `requests.get()`. Use `await asyncio.sleep()`, not `time.sleep()`. +- ❌ **Processing data inside try block**: Move data processing outside exception handlers. Only API calls belong in try blocks. +- ❌ **Hardcoded strings (not translatable)**: Use `translation_key` instead of `_attr_name = "Temperature Sensor"`. +- ❌ **Accessing hass.data directly in tests**: Use proper fixtures. +- ❌ **User-configurable polling intervals**: Integration determines this, not users. +- ❌ **Using standard library datetime**: Use `dt_util.now()` instead of `datetime.now()`. **See code for correct patterns:** -- Async operations: `api/client.py` -- Exception handling: `coordinator/core.py` -- Translations: `sensor/definitions.py` (translation_key usage) -- Test fixtures: `tests/conftest.py` -- Time handling: Any file importing `dt_util` + +- Async operations: `api/client.py` +- Exception handling: `coordinator/core.py` +- Translations: `sensor/definitions.py` (translation_key usage) +- Test fixtures: `tests/conftest.py` +- Time handling: Any file importing `dt_util` ## Recorder History Optimization **CRITICAL: Always exclude non-essential attributes from Recorder to prevent database bloat.** **Implementation:** + - Use `_unrecorded_attributes = frozenset({...})` as **class attribute** in entity classes - See `sensor/core.py` and `binary_sensor/core.py` for current implementation **What to exclude:** + 1. **Descriptions/help text** - `description`, `usage_tips` (static, large) 2. **Large nested structures** - `periods`, `data`, `*_attributes` dicts (>1KB) 3. **Frequently changing diagnostics** - `icon_color`, `cache_age`, status strings @@ -2592,14 +2614,15 @@ Only after consulting the official HA docs did we discover the correct pattern: 6. **Redundant/derived data** - `price_spread`, `diff_%` (calculable from other attrs) **What to keep:** + - `timestamp` (always), all price values, `cache_age_minutes`, `updates_today` - Period timing (`start`, `end`, `duration_minutes`), price statistics - Boolean status flags, `relaxation_active` **When adding new attributes:** + - Will this be useful in history 1 week from now? No → Exclude - Can this be calculated from other attributes? Yes → Exclude - Is this >100 bytes and not essential? Yes → Exclude **See:** `docs/developer/docs/recorder-optimization.md` for detailed categories and impact analysis - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de38ce7..1945758 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,14 +18,14 @@ For detailed developer documentation, see [docs/development/](docs/development/) 1. **Fork the repository** on GitHub 2. **Clone your fork**: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. **Open in DevContainer** (recommended): - - Open in VS Code - - Click "Reopen in Container" when prompted - - Or manually: `Ctrl+Shift+P` → "Dev Containers: Reopen in Container" + - Open in VS Code + - Click "Reopen in Container" when prompted + - Or manually: `Ctrl+Shift+P` → "Dev Containers: Reopen in Container" See [Development Setup](docs/development/setup.md) for detailed instructions. @@ -73,14 +73,17 @@ Impact: **Types:** `feat`, `fix`, `docs`, `refactor`, `chore`, `test` For full commit-message rules (including release-note skip trailers for internal/unreleased fixes), see: + - `.github/instructions/commit-messages.instructions.md` Important trailers for commits that should NOT appear in release notes: + - `Release-Notes: skip` - `User-Impact: none` - `Released-Bug: no` **Example:** + ```bash git commit -m "feat(sensors): add daily average price sensor @@ -97,9 +100,9 @@ See `.github/instructions/commit-messages.instructions.md` for detailed commit-m 1. **Push your branch** to your fork 2. **Create a Pull Request** on GitHub with: - - Clear title describing the change - - Detailed description with context - - Reference related issues (`Fixes #123`) + - Clear title describing the change + - Detailed description with context + - Reference related issues (`Fixes #123`) 3. **Wait for review** and address feedback ### PR Requirements @@ -119,6 +122,7 @@ See `.github/instructions/commit-messages.instructions.md` for detailed commit-m - **Python version**: 3.13+ Always run before committing: + ```bash ./scripts/lint ``` @@ -137,13 +141,14 @@ See [Coding Guidelines](docs/developer/docs/coding-guidelines.md) for complete d Documentation is organized in two Docusaurus sites: - **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation via `docs/user/sidebars.ts` + - Markdown files in `docs/user/docs/*.md` + - Navigation via `docs/user/sidebars.ts` - **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation via `docs/developer/sidebars.ts` + - Markdown files in `docs/developer/docs/*.md` + - Navigation via `docs/developer/sidebars.ts` **When adding new documentation:** + 1. Place file in appropriate `docs/*/docs/` directory 2. Add to corresponding `sidebars.ts` for navigation 3. Update translations when changing `translations/en.json` (update ALL language files) @@ -153,6 +158,7 @@ Documentation is organized in two Docusaurus sites: Report bugs via [GitHub Issues](../../issues/new/choose). **Great bug reports include:** + - Quick summary and background - Steps to reproduce (be specific!) - Expected vs. actual behavior diff --git a/README.md b/README.md index 4fcf66e..fd78930 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ **[📚 Complete Documentation](https://jpawlowski.github.io/hass.tibber_prices/)** — Installation, guides, examples, and full sensor reference: -- **[👤 User Documentation](https://jpawlowski.github.io/hass.tibber_prices/user/)** — Setup, sensors, automations, dashboards -- **[🔧 Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/)** — Architecture, contributing, development +- **[👤 User Documentation](https://jpawlowski.github.io/hass.tibber_prices/user/)** — Setup, sensors, automations, dashboards +- **[🔧 Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/)** — Architecture, contributing, development **Quick Links:** [Installation](https://jpawlowski.github.io/hass.tibber_prices/user/installation) · [Sensor Reference](https://jpawlowski.github.io/hass.tibber_prices/user/sensor-reference) · [Charts](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples) · [Automations](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples) · [FAQ](https://jpawlowski.github.io/hass.tibber_prices/user/faq) · [Changelog](https://github.com/jpawlowski/hass.tibber_prices/releases) @@ -34,39 +34,39 @@ Most Tibber integrations give you a single price sensor. This one gives you a ** ### 🔮 Know What's Coming -- **Quarter-hourly precision** — 15-minute interval prices, not just hourly averages -- **Price forecasts** — See average prices for the next 1h, 2h, 3h, ... up to 12h ahead -- **Trend analysis** — Know if prices are rising, falling, or stable — and when the next trend change happens -- **Price trajectory** — Detect turning points before they happen (first-half vs second-half window comparison) -- **Price outlook** — Instantly see if the next hours will be cheaper or more expensive than now +- **Quarter-hourly precision** — 15-minute interval prices, not just hourly averages +- **Price forecasts** — See average prices for the next 1h, 2h, 3h, ... up to 12h ahead +- **Trend analysis** — Know if prices are rising, falling, or stable — and when the next trend change happens +- **Price trajectory** — Detect turning points before they happen (first-half vs second-half window comparison) +- **Price outlook** — Instantly see if the next hours will be cheaper or more expensive than now ### ⚡ Automate Smartly -- **Best Price & Peak Price Periods** — Intelligent binary sensors that detect the cheapest and most expensive periods of the day, with configurable flexibility, relaxation strategies, and gap tolerance ([how it works](https://jpawlowski.github.io/hass.tibber_prices/user/period-calculation)) -- **Period timing sensors** — Duration, end time, remaining minutes, progress percentage, and countdown to next period — everything you need for advanced automations -- **Runtime configuration** — Adjust period detection parameters on the fly via switches and number entities, without restarting — perfect for automations that adapt to your schedule -- **5-level price classification** — VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE from Tibber's API -- **3-level price ratings** — LOW, NORMAL, HIGH based on 24h trailing average comparison +- **Best Price & Peak Price Periods** — Intelligent binary sensors that detect the cheapest and most expensive periods of the day, with configurable flexibility, relaxation strategies, and gap tolerance ([how it works](https://jpawlowski.github.io/hass.tibber_prices/user/period-calculation)) +- **Period timing sensors** — Duration, end time, remaining minutes, progress percentage, and countdown to next period — everything you need for advanced automations +- **Runtime configuration** — Adjust period detection parameters on the fly via switches and number entities, without restarting — perfect for automations that adapt to your schedule +- **5-level price classification** — VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE from Tibber's API +- **3-level price ratings** — LOW, NORMAL, HIGH based on 24h trailing average comparison ### 📊 Visualize Beautifully -- **Auto-generated ApexCharts** — One action call generates a complete chart configuration with dynamic Y-axis scaling and color-coded price levels ([see examples](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples)) -- **Dynamic icons & colors** — Every sensor adapts its icon and color to the current price state — cheap prices glow green, expensive ones turn red ([icon guide](https://jpawlowski.github.io/hass.tibber_prices/user/dynamic-icons)) -- **Chart data export** — Flexible data API with filtering, resolution control, and multiple output formats for any visualization card +- **Auto-generated ApexCharts** — One action call generates a complete chart configuration with dynamic Y-axis scaling and color-coded price levels ([see examples](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples)) +- **Dynamic icons & colors** — Every sensor adapts its icon and color to the current price state — cheap prices glow green, expensive ones turn red ([icon guide](https://jpawlowski.github.io/hass.tibber_prices/user/dynamic-icons)) +- **Chart data export** — Flexible data API with filtering, resolution control, and multiple output formats for any visualization card ### 📈 Understand Your Market -- **Volatility analysis** — Know if today's prices are stable or wild (low/moderate/high/very_high) -- **Daily & rolling statistics** — Min, max, average, median for today, tomorrow, trailing 24h, and leading 24h -- **Energy & tax breakdown** — See spot price vs. tax components as sensor attributes -- **Multi-currency support** — EUR, NOK, SEK, DKK, USD, GBP with configurable base/subunit display (€ vs ct, kr vs øre) +- **Volatility analysis** — Know if today's prices are stable or wild (low/moderate/high/very_high) +- **Daily & rolling statistics** — Min, max, average, median for today, tomorrow, trailing 24h, and leading 24h +- **Energy & tax breakdown** — See spot price vs. tax components as sensor attributes +- **Multi-currency support** — EUR, NOK, SEK, DKK, USD, GBP with configurable base/subunit display (€ vs ct, kr vs øre) ### 🛡️ Built for Reliability -- **Intelligent caching** — Multi-layer caching minimizes API calls, survives HA restarts, auto-invalidates at midnight -- **High-performance interval pool** — O(1) timestamp lookups, gap detection, auto-fetching of missing data -- **Quarter-hour precision updates** — Sensors refresh at :00/:15/:30/:45 boundaries, independent of API polling -- **Official API only** — Uses Tibber's [`priceInfo`](https://developer.tibber.com/docs/reference#priceinfo) and [`priceInfoRange`](https://developer.tibber.com/docs/reference#subscription) endpoints. All ratings and statistics are calculated locally. +- **Intelligent caching** — Multi-layer caching minimizes API calls, survives HA restarts, auto-invalidates at midnight +- **High-performance interval pool** — O(1) timestamp lookups, gap detection, auto-fetching of missing data +- **Quarter-hour precision updates** — Sensors refresh at :00/:15/:30/:45 boundaries, independent of API polling +- **Official API only** — Uses Tibber's [`priceInfo`](https://developer.tibber.com/docs/reference#priceinfo) and [`priceInfoRange`](https://developer.tibber.com/docs/reference#subscription) endpoints. All ratings and statistics are calculated locally. ## 🚀 Quick Start @@ -91,9 +91,9 @@ Or manually: **Settings** → **Devices & Services** → **+ Add Integration** ### Step 3: Done! -- **100+ sensors** are now available (key sensors enabled by default, advanced ones ready to enable) -- Explore entities in **Settings** → **Devices & Services** → **Tibber Price Information & Ratings** -- Start building automations, dashboards, and energy-saving workflows +- **100+ sensors** are now available (key sensors enabled by default, advanced ones ready to enable) +- Explore entities in **Settings** → **Devices & Services** → **Tibber Price Information & Ratings** +- Start building automations, dashboards, and energy-saving workflows 📖 **[Full Installation Guide →](https://jpawlowski.github.io/hass.tibber_prices/user/installation)** @@ -103,18 +103,18 @@ The integration provides **100+ entities** across sensors, binary sensors, switc Entity list showing dynamic icons for different price states -| Category | Highlights | Count | -|----------|-----------|-------| -| **💰 Prices** | Current, next & previous interval price + rolling hour averages | 6+ | -| **📊 Statistics** | Daily min/max/avg for today & tomorrow, 24h trailing & leading windows | 12+ | -| **🔮 Forecasts** | Next 1h–12h average prices, price outlook & trajectory sensors | 20+ | -| **📈 Trends** | Current trend direction, next trend change time & countdown | 3 | -| **📉 Volatility** | Today, tomorrow, next 24h & combined volatility levels | 4 | -| **🏷️ Levels & Ratings** | 5-level (API) and 3-level (computed) classification per interval, hour & day | 12+ | -| **⏰ Period Timing** | Best/peak: end time, duration, remaining, progress, next start | 10+ | -| **🔌 Binary Sensors** | Best price period, peak price period, tomorrow data available, API connection | 4+ | -| **🎛️ Runtime Config** | Switches & numbers to adjust period detection live — no restart needed | 14 | -| **🔧 Diagnostics** | Data lifecycle status, home metadata, grid info, subscription status | 15+ | +| Category | Highlights | Count | +| ----------------------- | ----------------------------------------------------------------------------- | ----- | +| **💰 Prices** | Current, next & previous interval price + rolling hour averages | 6+ | +| **📊 Statistics** | Daily min/max/avg for today & tomorrow, 24h trailing & leading windows | 12+ | +| **🔮 Forecasts** | Next 1h–12h average prices, price outlook & trajectory sensors | 20+ | +| **📈 Trends** | Current trend direction, next trend change time & countdown | 3 | +| **📉 Volatility** | Today, tomorrow, next 24h & combined volatility levels | 4 | +| **🏷️ Levels & Ratings** | 5-level (API) and 3-level (computed) classification per interval, hour & day | 12+ | +| **⏰ Period Timing** | Best/peak: end time, duration, remaining, progress, next start | 10+ | +| **🔌 Binary Sensors** | Best price period, peak price period, tomorrow data available, API connection | 4+ | +| **🎛️ Runtime Config** | Switches & numbers to adjust period detection live — no restart needed | 14 | +| **🔧 Diagnostics** | Data lifecycle status, home metadata, grid info, subscription status | 15+ | > **Every sensor includes rich attributes** — timestamps, detailed descriptions, and context data. Enable **Extended Descriptions** in the integration options to get `long_description` and `usage_tips` on every entity. @@ -168,17 +168,17 @@ Generate beautiful price charts with a single action call — dynamic Y-axis, co ## ❓ Help & Support -- 📖 **[FAQ](https://jpawlowski.github.io/hass.tibber_prices/user/faq)** — Common questions answered -- 🔧 **[Troubleshooting](https://jpawlowski.github.io/hass.tibber_prices/user/troubleshooting)** — Solving common issues -- 🐛 **[Report an Issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new)** — Found a bug? Let us know +- 📖 **[FAQ](https://jpawlowski.github.io/hass.tibber_prices/user/faq)** — Common questions answered +- 🔧 **[Troubleshooting](https://jpawlowski.github.io/hass.tibber_prices/user/troubleshooting)** — Solving common issues +- 🐛 **[Report an Issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new)** — Found a bug? Let us know ## 🤝 Contributing Contributions are welcome! See the [Contributing Guidelines](CONTRIBUTING.md) and [Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/) to get started. -- **[Developer Setup](https://jpawlowski.github.io/hass.tibber_prices/developer/setup)** — DevContainer-based development environment -- **[Architecture Guide](https://jpawlowski.github.io/hass.tibber_prices/developer/architecture)** — Understand the codebase -- **[Release Management](https://jpawlowski.github.io/hass.tibber_prices/developer/release-management)** — Release process and versioning +- **[Developer Setup](https://jpawlowski.github.io/hass.tibber_prices/developer/setup)** — DevContainer-based development environment +- **[Architecture Guide](https://jpawlowski.github.io/hass.tibber_prices/developer/architecture)** — Understand the codebase +- **[Release Management](https://jpawlowski.github.io/hass.tibber_prices/developer/release-management)** — Release process and versioning ## 🤖 Development Note diff --git a/docs/developer/docs/api-reference.md b/docs/developer/docs/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/docs/api-reference.md +++ b/docs/developer/docs/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/docs/architecture.md b/docs/developer/docs/architecture.md index 1a93778..7046c88 100644 --- a/docs/developer/docs/architecture.md +++ b/docs/developer/docs/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/docs/caching-strategy.md b/docs/developer/docs/caching-strategy.md index 40d7386..ce342a3 100644 --- a/docs/developer/docs/caching-strategy.md +++ b/docs/developer/docs/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/docs/coding-guidelines.md b/docs/developer/docs/coding-guidelines.md index a565afd..a336576 100644 --- a/docs/developer/docs/coding-guidelines.md +++ b/docs/developer/docs/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/docs/contributing.md b/docs/developer/docs/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/docs/contributing.md +++ b/docs/developer/docs/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/docs/critical-patterns.md b/docs/developer/docs/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/docs/critical-patterns.md +++ b/docs/developer/docs/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/docs/debugging.md b/docs/developer/docs/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/docs/debugging.md +++ b/docs/developer/docs/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/docs/intro.md b/docs/developer/docs/intro.md index 7837f7e..089921d 100644 --- a/docs/developer/docs/intro.md +++ b/docs/developer/docs/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/docs/performance.md b/docs/developer/docs/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/docs/performance.md +++ b/docs/developer/docs/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/docs/period-calculation-theory.md b/docs/developer/docs/period-calculation-theory.md index cea1f69..890464e 100644 --- a/docs/developer/docs/period-calculation-theory.md +++ b/docs/developer/docs/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -397,6 +436,7 @@ These three mechanisms handle pathological price situations where standard filte **Problem:** When all prices are nearly identical (e.g. 28–32 ct, CV=5.4%), requiring 2 distinct "best price" windows is geometrically impossible. Even after exhausting all 11 relaxation phases, only 1 period exists because there is no second cheap cluster. **Solution:** Before the baseline counting loop, compute per-day effective min_periods: + ```python if day_cv <= 10%: day_effective_min[day] = 1 # Flat day: 1 period is enough @@ -417,17 +457,18 @@ else: **Problem:** On solar surplus days (avg 2–5 ct/kWh), a percentage-based min_distance like 5% means only 0.1 ct absolute separation is required. The filter either accepts almost the entire day (if ref_price is 2 ct, 5% = 0.1 ct – nearly everything qualifies) or blocks everything (if the spread is within that 0.1 ct band). **Solution:** Linear scaling toward zero as avg_price approaches zero: + ``` scale_factor = avg_price / LOW_PRICE_AVG_THRESHOLD adjusted_min_distance = original_min_distance × scale_factor ``` -| avg_price | scale | Effect on 5% min_distance | -|---|---|---| -| ≥ 10 ct (0.10 EUR) | 100% | 5% (full distance) | -| 5 ct (0.05 EUR) | 50% | 2.5% | -| 2 ct (0.02 EUR) | 20% | 1% | -| 0 ct | 0% | 0% (disabled) | +| avg_price | scale | Effect on 5% min_distance | +| ------------------ | ----- | ------------------------- | +| ≥ 10 ct (0.10 EUR) | 100% | 5% (full distance) | +| 5 ct (0.05 EUR) | 50% | 2.5% | +| 2 ct (0.02 EUR) | 20% | 1% | +| 0 ct | 0% | 0% (disabled) | ### 3. CV Quality Gate Bypass for Absolute Low-Price Periods @@ -435,11 +476,12 @@ adjusted_min_distance = original_min_distance × scale_factor **Trigger:** Period mean price < `LOW_PRICE_QUALITY_BYPASS_THRESHOLD` (0.10 EUR) -**Problem:** A period at 0.5–4 ct has high *relative* variation (CV ≈ 70–80%), but the absolute differences are fractions of a cent. The quality gate (CV ≤ `PERIOD_MAX_CV`) with a relative metric would wrongly reject this as a "heterogeneous" period. +**Problem:** A period at 0.5–4 ct has high _relative_ variation (CV ≈ 70–80%), but the absolute differences are fractions of a cent. The quality gate (CV ≤ `PERIOD_MAX_CV`) with a relative metric would wrongly reject this as a "heterogeneous" period. **Distinguishes from flat normal days:** A flat day at 33–36 ct also has low absolute range, but mean is 34.5 ct (>> 0.10 EUR threshold). The bypass only applies when the mean itself is below the threshold – i.e. the day is genuinely cheap in absolute terms. **Solution:** Short-circuit the quality gate check: + ```python period_mean = sum(period_prices) / len(period_prices) if period_mean < LOW_PRICE_QUALITY_BYPASS_THRESHOLD: @@ -470,6 +512,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -493,6 +536,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -502,26 +546,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -529,6 +574,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -547,12 +593,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -567,6 +615,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -577,6 +626,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -586,6 +636,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -601,43 +652,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -648,18 +701,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -673,19 +729,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -699,15 +755,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -720,52 +778,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -781,11 +848,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -799,11 +868,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -825,6 +896,7 @@ Relaxation would exhaust all 11 phases trying to find a second period. All price `_compute_day_effective_min()` detects CV ≤ 10% and sets `day_effective_min = 1` for this day. The result is accepted after finding the single cheapest cluster. **Expected Logs:** + ``` DEBUG: Day 2025-11-11: flat price profile (CV=5.4% ≤ 10.0%) → min_periods relaxed to 1 INFO: Adaptive min_periods: 1 flat day(s) (CV ≤ 10%) need only 1 period instead of 2 @@ -832,9 +904,10 @@ INFO: Day 2025-11-11: Baseline satisfied (1 period, effective minimum is 1) ``` **Sensor Attributes:** + ```yaml -min_periods_configured: 2 # User's setting -flat_days_detected: 1 # Explains why only 1 period found +min_periods_configured: 2 # User's setting +flat_days_detected: 1 # Explains why only 1 period found ``` **Why not for Peak Price?** @@ -847,18 +920,20 @@ Peak price always runs full relaxation. On a flat day, the integration still nee **Configuration:** `min_periods_best: 2`, 5% min_distance **Problems without fixes:** -1. **min_distance conflict:** 5% of 2.1 ct = 0.105 ct minimum distance. Only prices ≤ 1.995 ct qualify. The daily minimum is 0.5 ct – well within range. But the *relative* threshold becomes meaninglessly tiny: the entire day could qualify. + +1. **min_distance conflict:** 5% of 2.1 ct = 0.105 ct minimum distance. Only prices ≤ 1.995 ct qualify. The daily minimum is 0.5 ct – well within range. But the _relative_ threshold becomes meaninglessly tiny: the entire day could qualify. 2. **CV quality gate:** Prices 0.5–4.2 ct show high relative variation (CV ≈ 70-80%), but the absolute differences are fractions of a cent. The quality gate would wrongly reject valid periods. **Implemented behavior:** -*`LOW_PRICE_AVG_THRESHOLD = 0.10 EUR` (level_filtering.py):* +_`LOW_PRICE_AVG_THRESHOLD = 0.10 EUR` (level_filtering.py):_ When `avg_price < 0.10 EUR`, min_distance is scaled linearly to 0. At avg=2.1 ct (0.021 EUR), scale ≈ 21% → min_distance effectively 1%. Prevents the distance filter from blocking the entire day or accepting the entire day. -*`LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 EUR` (relaxation.py):* +_`LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 EUR` (relaxation.py):_ When period mean < 0.10 EUR, the CV quality gate is bypassed entirely. A period at 0.5–2 ct with CV=60% is practically homogeneous from a cost perspective. **Expected Logs:** + ``` DEBUG: Low-price day (avg=0.021 EUR < 0.10 threshold): min_distance scaled 5% → 1.1% DEBUG: Period 02:00-05:00: mean=0.009 EUR < bypass threshold → quality gate bypassed @@ -870,11 +945,13 @@ DEBUG: Period 02:00-05:00: mean=0.009 EUR < bypass threshold → quality gate **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -889,6 +966,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -904,6 +982,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -917,49 +996,49 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels 6. **Check Flat Day / Low-Price Adaptations** - - Sensor shows `flat_days_detected: N` → CV ≤ 10%, adaptive min_periods reduced target to 1 - - Sensor shows `relaxation_incomplete: true` without `flat_days_detected` → check filter settings - - Low absolute prices (avg < 10 ct): min_distance is auto-scaled, CV quality gate is bypassed - - To confirm: Enable DEBUG logging for `custom_components.tibber_prices.coordinator.period_handlers.relaxation.details` - - Look for: `"flat price profile (CV=X.X% ≤ 10.0%) → min_periods relaxed to 1"` - - Look for: `"Low-price day (avg=0.0XX EUR < 0.10 threshold): min_distance scaled X% → Y%"` + - Sensor shows `flat_days_detected: N` → CV ≤ 10%, adaptive min_periods reduced target to 1 + - Sensor shows `relaxation_incomplete: true` without `flat_days_detected` → check filter settings + - Low absolute prices (avg < 10 ct): min_distance is auto-scaled, CV quality gate is bypassed + - To confirm: Enable DEBUG logging for `custom_components.tibber_prices.coordinator.period_handlers.relaxation.details` + - Look for: `"flat price profile (CV=X.X% ≤ 10.0%) → min_periods relaxed to 1"` + - Look for: `"Low-price day (avg=0.0XX EUR < 0.10 threshold): min_distance scaled X% → Y%"` **Diagnostic Sensor Attributes Summary:** -| Attribute | Type | When shown | Meaning | -|---|---|---|---| -| `min_periods_configured` | int | Always | User's configured target per day | -| `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | -| `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | -| `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | -| `relaxation_level` | string | Only when active | Flex% and filter combo that succeeded | +| Attribute | Type | When shown | Meaning | +| ------------------------ | ------ | ---------------- | ------------------------------------------- | +| `min_periods_configured` | int | Always | User's configured target per day | +| `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | +| `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | +| `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | +| `relaxation_level` | string | Only when active | Flex% and filter combo that succeeded | --- @@ -968,19 +1047,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -999,6 +1078,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -1012,11 +1092,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -1028,17 +1110,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -1051,22 +1136,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -1081,14 +1170,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -1098,6 +1190,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -1107,6 +1200,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -1116,15 +1210,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -1136,6 +1233,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1187,6 +1285,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1198,6 +1297,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1206,15 +1306,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1222,24 +1324,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/docs/recorder-optimization.md b/docs/developer/docs/recorder-optimization.md index 60d5aa0..65dffe2 100644 --- a/docs/developer/docs/recorder-optimization.md +++ b/docs/developer/docs/recorder-optimization.md @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `timestamp`, `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - `timestamp` is the rounded-quarter reference time used at the moment of the state write — it's stale as soon as the next update fires and has no analytical value in history - `next_api_poll`, `next_midnight_turnover` etc. are only relevant at the moment of reading; they're superseded by the next update - Similar to `entity_picture` in HA core image entities @@ -129,6 +137,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -140,6 +149,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `period_count_total`, `period_count_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -153,23 +163,28 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - All price values - Core sensor states (the entity's `native_value` is always recorded separately) ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics - `period_position` - Position of current period in the day's sequence - `period_count_today`, `period_count_tomorrow` - How many periods per day (useful in automations) ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -177,6 +192,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -198,6 +214,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -209,14 +226,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 46 attributes excluded + - Class: `TibberPricesSensor` + - 46 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 29 attributes excluded + - Class: `TibberPricesBinarySensor` + - 29 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -238,24 +255,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -267,6 +284,7 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql SELECT state_id, @@ -300,11 +318,11 @@ This makes `state_class=TOTAL` on many sensors the primary cause of long-term da For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_class` values are valid: -| `state_class` | Statistics written | Frontend effect | -|---|---|---| -| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page | -| `None` | ❌ No | States timeline only (History panel, "Show More") | -| `MEASUREMENT` | ❌ Blocked by hassfest | — | +| `state_class` | Statistics written | Frontend effect | +| ------------- | ------------------------- | ------------------------------------------------- | +| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page | +| `None` | ❌ No | States timeline only (History panel, "Show More") | +| `MEASUREMENT` | ❌ Blocked by hassfest | — | `MEASUREMENT` causes a hassfest validation error for MONETARY sensors, leaving only `TOTAL` or `None`. @@ -312,19 +330,21 @@ For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_clas Only 3 of 26 MONETARY sensors keep `state_class=TOTAL` — those where long-term history is genuinely useful: -| Sensor | Reason | -|---|---| -| `current_interval_price` | Long-term price trend (weeks/months) | -| `current_interval_price_base` | Required for Energy Dashboard | -| `average_price_today` | Seasonal daily average tracking | +| Sensor | Reason | +| ----------------------------- | ------------------------------------ | +| `current_interval_price` | Long-term price trend (weeks/months) | +| `current_interval_price_base` | Required for Energy Dashboard | +| `average_price_today` | Seasonal daily average tracking | All other 23 MONETARY sensors use `state_class=None`: + - Forecast/future sensors (`next_avg_*h`) - Daily snapshots (`lowest/highest_price_today/tomorrow`) - Rolling windows (`trailing/leading_24h_*`) - Next/previous interval sensors **Effect of `state_class=None`:** + - ✅ Short-term state history (States timeline, ~10 days) still works normally - ✅ Templates, automations, and attributes are unaffected - ❌ Statistics line-chart removed from entity detail page for these sensors @@ -333,6 +353,7 @@ All other 23 MONETARY sensors use `state_class=None`: ### Expected Impact Going from 26 → 3 sensors writing to the statistics tables: + - **~88% reduction** in statistics table writes - Prevents the primary cause of long-term database bloat - Existing statistics data is retained (only new writes stop) @@ -341,10 +362,10 @@ Going from 26 → 3 sensors writing to the statistics tables: These are two independent mechanisms targeting different tables: -| Mechanism | Table affected | Purged? | Controls | -|---|---|---|---| -| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write | -| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all | +| Mechanism | Table affected | Purged? | Controls | +| ------------------------ | ------------------------------------- | ----------- | ----------------------------------------------- | +| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write | +| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all | Both optimizations work together. `_unrecorded_attributes` reduces the size of each state write; `state_class=None` eliminates an entire category of unbounded writes. diff --git a/docs/developer/docs/refactoring-guide.md b/docs/developer/docs/refactoring-guide.md index 2181fc8..7e2ce75 100644 --- a/docs/developer/docs/refactoring-guide.md +++ b/docs/developer/docs/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENT ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/docs/release-management.md b/docs/developer/docs/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/docs/release-management.md +++ b/docs/developer/docs/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/docs/repairs-system.md b/docs/developer/docs/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/docs/repairs-system.md +++ b/docs/developer/docs/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/docs/setup.md b/docs/developer/docs/setup.md index d5f4d3f..5ac16b0 100644 --- a/docs/developer/docs/setup.md +++ b/docs/developer/docs/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/docs/testing.md b/docs/developer/docs/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/docs/testing.md +++ b/docs/developer/docs/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/docs/timer-architecture.md b/docs/developer/docs/timer-architecture.md index 6abd1c1..606e10f 100644 --- a/docs/developer/docs/timer-architecture.md +++ b/docs/developer/docs/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.21.0/api-reference.md b/docs/developer/versioned_docs/version-v0.21.0/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.21.0/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.21.0/architecture.md b/docs/developer/versioned_docs/version-v0.21.0/architecture.md index 1a93778..7046c88 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.21.0/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.21.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.21.0/caching-strategy.md index 40d7386..ce342a3 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.21.0/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.21.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.21.0/coding-guidelines.md index a565afd..a336576 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.21.0/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.21.0/contributing.md b/docs/developer/versioned_docs/version-v0.21.0/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/contributing.md +++ b/docs/developer/versioned_docs/version-v0.21.0/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.21.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.21.0/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.21.0/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.21.0/debugging.md b/docs/developer/versioned_docs/version-v0.21.0/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.21.0/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.21.0/intro.md b/docs/developer/versioned_docs/version-v0.21.0/intro.md index 7837f7e..089921d 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/intro.md +++ b/docs/developer/versioned_docs/version-v0.21.0/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.21.0/performance.md b/docs/developer/versioned_docs/version-v0.21.0/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.21.0/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.21.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.21.0/period-calculation-theory.md index 66cd87b..c381ab9 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.21.0/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -395,6 +434,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -418,6 +458,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -427,26 +468,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -454,6 +496,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -472,12 +515,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -492,6 +537,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -502,6 +548,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -511,6 +558,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -526,43 +574,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -573,18 +623,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -598,19 +651,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -624,15 +677,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -645,52 +700,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -706,11 +770,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -724,11 +790,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -743,11 +811,13 @@ DEBUG: Day 2025-11-11: Baseline insufficient (1 < 2), starting relaxation **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -762,6 +832,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -777,6 +848,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -790,31 +862,31 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels --- @@ -823,19 +895,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -854,6 +926,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -867,11 +940,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -883,17 +958,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -906,22 +984,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -936,14 +1018,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -953,6 +1038,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -962,6 +1048,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -971,15 +1058,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -991,6 +1081,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1042,6 +1133,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1053,6 +1145,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1061,15 +1154,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1077,24 +1172,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.21.0/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.21.0/recorder-optimization.md index 75f2abb..984dcc0 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.21.0/recorder-optimization.md @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - Only relevant at moment of reading - Won't be valid after some time - Similar to `entity_picture` in HA core image entities @@ -128,6 +136,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -139,6 +148,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -152,22 +162,27 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - `timestamp` - Critical for time-series analysis (ALWAYS FIRST) - All price values - Core sensor states ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 47 attributes excluded + - Class: `TibberPricesSensor` + - 47 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 30 attributes excluded + - Class: `TibberPricesBinarySensor` + - 30 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,6 +282,7 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql SELECT state_id, diff --git a/docs/developer/versioned_docs/version-v0.21.0/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.21.0/refactoring-guide.md index 2181fc8..7e2ce75 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.21.0/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENT ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.21.0/release-management.md b/docs/developer/versioned_docs/version-v0.21.0/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/release-management.md +++ b/docs/developer/versioned_docs/version-v0.21.0/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.21.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.21.0/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.21.0/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.21.0/setup.md b/docs/developer/versioned_docs/version-v0.21.0/setup.md index d5f4d3f..5ac16b0 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/setup.md +++ b/docs/developer/versioned_docs/version-v0.21.0/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.21.0/testing.md b/docs/developer/versioned_docs/version-v0.21.0/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/testing.md +++ b/docs/developer/versioned_docs/version-v0.21.0/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md index 6abd1c1..606e10f 100644 --- a/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.22.0/api-reference.md b/docs/developer/versioned_docs/version-v0.22.0/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.22.0/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.22.0/architecture.md b/docs/developer/versioned_docs/version-v0.22.0/architecture.md index 1a93778..7046c88 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.22.0/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.22.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.22.0/caching-strategy.md index 40d7386..ce342a3 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.22.0/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.22.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.22.0/coding-guidelines.md index a565afd..a336576 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.22.0/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.22.0/contributing.md b/docs/developer/versioned_docs/version-v0.22.0/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/contributing.md +++ b/docs/developer/versioned_docs/version-v0.22.0/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.22.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.22.0/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.22.0/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.22.0/debugging.md b/docs/developer/versioned_docs/version-v0.22.0/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.22.0/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.22.0/intro.md b/docs/developer/versioned_docs/version-v0.22.0/intro.md index 7837f7e..089921d 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/intro.md +++ b/docs/developer/versioned_docs/version-v0.22.0/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.22.0/performance.md b/docs/developer/versioned_docs/version-v0.22.0/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.22.0/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.22.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.22.0/period-calculation-theory.md index 66cd87b..c381ab9 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.22.0/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -395,6 +434,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -418,6 +458,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -427,26 +468,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -454,6 +496,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -472,12 +515,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -492,6 +537,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -502,6 +548,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -511,6 +558,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -526,43 +574,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -573,18 +623,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -598,19 +651,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -624,15 +677,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -645,52 +700,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -706,11 +770,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -724,11 +790,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -743,11 +811,13 @@ DEBUG: Day 2025-11-11: Baseline insufficient (1 < 2), starting relaxation **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -762,6 +832,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -777,6 +848,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -790,31 +862,31 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels --- @@ -823,19 +895,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -854,6 +926,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -867,11 +940,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -883,17 +958,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -906,22 +984,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -936,14 +1018,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -953,6 +1038,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -962,6 +1048,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -971,15 +1058,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -991,6 +1081,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1042,6 +1133,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1053,6 +1145,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1061,15 +1154,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1077,24 +1172,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.22.0/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.22.0/recorder-optimization.md index a40d78f..984dcc0 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.22.0/recorder-optimization.md @@ -18,7 +18,7 @@ Both `TibberPricesSensor` and `TibberPricesBinarySensor` implement `_unrecorded_ ```python class TibberPricesSensor(TibberPricesEntity, SensorEntity): """tibber_prices Sensor class.""" - + _unrecorded_attributes = frozenset( { "description", @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - Only relevant at moment of reading - Won't be valid after some time - Similar to `entity_picture` in HA core image entities @@ -128,6 +136,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -139,6 +148,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -152,22 +162,27 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - `timestamp` - Critical for time-series analysis (ALWAYS FIRST) - All price values - Core sensor states ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 47 attributes excluded + - Class: `TibberPricesSensor` + - 47 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 30 attributes excluded + - Class: `TibberPricesBinarySensor` + - 30 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,13 +282,14 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql -SELECT +SELECT state_id, attributes -FROM states +FROM states WHERE entity_id = 'sensor.tibber_home_current_interval_price' -ORDER BY last_updated DESC +ORDER BY last_updated DESC LIMIT 5; ``` diff --git a/docs/developer/versioned_docs/version-v0.22.0/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.22.0/refactoring-guide.md index 2181fc8..7e2ce75 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.22.0/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENT ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.22.0/release-management.md b/docs/developer/versioned_docs/version-v0.22.0/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/release-management.md +++ b/docs/developer/versioned_docs/version-v0.22.0/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.22.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.22.0/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.22.0/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.22.0/setup.md b/docs/developer/versioned_docs/version-v0.22.0/setup.md index d5f4d3f..5ac16b0 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/setup.md +++ b/docs/developer/versioned_docs/version-v0.22.0/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.22.0/testing.md b/docs/developer/versioned_docs/version-v0.22.0/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/testing.md +++ b/docs/developer/versioned_docs/version-v0.22.0/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.22.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.22.0/timer-architecture.md index 6abd1c1..606e10f 100644 --- a/docs/developer/versioned_docs/version-v0.22.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.22.0/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.22.1/api-reference.md b/docs/developer/versioned_docs/version-v0.22.1/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.22.1/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.22.1/architecture.md b/docs/developer/versioned_docs/version-v0.22.1/architecture.md index 1a93778..7046c88 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/architecture.md +++ b/docs/developer/versioned_docs/version-v0.22.1/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.22.1/caching-strategy.md b/docs/developer/versioned_docs/version-v0.22.1/caching-strategy.md index 40d7386..ce342a3 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.22.1/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.22.1/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.22.1/coding-guidelines.md index a565afd..a336576 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.22.1/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.22.1/contributing.md b/docs/developer/versioned_docs/version-v0.22.1/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/contributing.md +++ b/docs/developer/versioned_docs/version-v0.22.1/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.22.1/critical-patterns.md b/docs/developer/versioned_docs/version-v0.22.1/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.22.1/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.22.1/debugging.md b/docs/developer/versioned_docs/version-v0.22.1/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/debugging.md +++ b/docs/developer/versioned_docs/version-v0.22.1/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.22.1/intro.md b/docs/developer/versioned_docs/version-v0.22.1/intro.md index 9b8eec1..0e9b822 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/intro.md +++ b/docs/developer/versioned_docs/version-v0.22.1/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.22.1/performance.md b/docs/developer/versioned_docs/version-v0.22.1/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/performance.md +++ b/docs/developer/versioned_docs/version-v0.22.1/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.22.1/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.22.1/period-calculation-theory.md index cb1b3f1..db45cb1 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.22.1/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -395,6 +434,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -418,6 +458,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -427,26 +468,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -454,6 +496,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -472,12 +515,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -492,6 +537,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -502,6 +548,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -511,6 +558,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -526,43 +574,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -573,18 +623,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -598,19 +651,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -624,15 +677,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -645,52 +700,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -706,11 +770,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -724,11 +790,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -743,11 +811,13 @@ DEBUG: Day 2025-11-11: Baseline insufficient (1 < 2), starting relaxation **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -762,6 +832,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -777,6 +848,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -790,31 +862,31 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels --- @@ -823,19 +895,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -854,6 +926,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -867,11 +940,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -883,17 +958,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -906,22 +984,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -936,14 +1018,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -953,6 +1038,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -962,6 +1048,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -971,15 +1058,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -991,6 +1081,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1042,6 +1133,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1053,6 +1145,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1061,15 +1154,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1077,24 +1172,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.22.1/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.22.1/recorder-optimization.md index a40d78f..984dcc0 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.22.1/recorder-optimization.md @@ -18,7 +18,7 @@ Both `TibberPricesSensor` and `TibberPricesBinarySensor` implement `_unrecorded_ ```python class TibberPricesSensor(TibberPricesEntity, SensorEntity): """tibber_prices Sensor class.""" - + _unrecorded_attributes = frozenset( { "description", @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - Only relevant at moment of reading - Won't be valid after some time - Similar to `entity_picture` in HA core image entities @@ -128,6 +136,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -139,6 +148,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -152,22 +162,27 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - `timestamp` - Critical for time-series analysis (ALWAYS FIRST) - All price values - Core sensor states ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 47 attributes excluded + - Class: `TibberPricesSensor` + - 47 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 30 attributes excluded + - Class: `TibberPricesBinarySensor` + - 30 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,13 +282,14 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql -SELECT +SELECT state_id, attributes -FROM states +FROM states WHERE entity_id = 'sensor.tibber_home_current_interval_price' -ORDER BY last_updated DESC +ORDER BY last_updated DESC LIMIT 5; ``` diff --git a/docs/developer/versioned_docs/version-v0.22.1/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.22.1/refactoring-guide.md index 2181fc8..7e2ce75 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.22.1/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/main/AGENT ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.22.1/release-management.md b/docs/developer/versioned_docs/version-v0.22.1/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/release-management.md +++ b/docs/developer/versioned_docs/version-v0.22.1/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.22.1/repairs-system.md b/docs/developer/versioned_docs/version-v0.22.1/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.22.1/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.22.1/setup.md b/docs/developer/versioned_docs/version-v0.22.1/setup.md index d5f4d3f..5ac16b0 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/setup.md +++ b/docs/developer/versioned_docs/version-v0.22.1/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.22.1/testing.md b/docs/developer/versioned_docs/version-v0.22.1/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/testing.md +++ b/docs/developer/versioned_docs/version-v0.22.1/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.22.1/timer-architecture.md b/docs/developer/versioned_docs/version-v0.22.1/timer-architecture.md index 6abd1c1..606e10f 100644 --- a/docs/developer/versioned_docs/version-v0.22.1/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.22.1/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.23.0/api-reference.md b/docs/developer/versioned_docs/version-v0.23.0/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.23.0/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.23.0/architecture.md b/docs/developer/versioned_docs/version-v0.23.0/architecture.md index 9c493a9..46db413 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.23.0/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.23.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.23.0/caching-strategy.md index 8391859..3844348 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.23.0/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.23.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.23.0/coding-guidelines.md index 0ca62bd..fb25b96 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.23.0/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.23.0/contributing.md b/docs/developer/versioned_docs/version-v0.23.0/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/contributing.md +++ b/docs/developer/versioned_docs/version-v0.23.0/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.23.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.23.0/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.23.0/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.23.0/debugging.md b/docs/developer/versioned_docs/version-v0.23.0/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.23.0/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.23.0/intro.md b/docs/developer/versioned_docs/version-v0.23.0/intro.md index 96df791..1d7e321 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/intro.md +++ b/docs/developer/versioned_docs/version-v0.23.0/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.0/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.0/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.0/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.23.0/performance.md b/docs/developer/versioned_docs/version-v0.23.0/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.23.0/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.23.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.23.0/period-calculation-theory.md index e25e1ba..d40776c 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.23.0/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -395,6 +434,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -418,6 +458,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -427,26 +468,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -454,6 +496,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -472,12 +515,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -492,6 +537,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -502,6 +548,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -511,6 +558,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -526,43 +574,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -573,18 +623,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -598,19 +651,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -624,15 +677,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -645,52 +700,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -706,11 +770,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -724,11 +790,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -743,11 +811,13 @@ DEBUG: Day 2025-11-11: Baseline insufficient (1 < 2), starting relaxation **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -762,6 +832,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -777,6 +848,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -790,31 +862,31 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels --- @@ -823,19 +895,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -854,6 +926,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -867,11 +940,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -883,17 +958,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -906,22 +984,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -936,14 +1018,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -953,6 +1038,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -962,6 +1048,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -971,15 +1058,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -991,6 +1081,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1042,6 +1133,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1053,6 +1145,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1061,15 +1154,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1077,24 +1172,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.23.0/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.23.0/recorder-optimization.md index a40d78f..984dcc0 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.23.0/recorder-optimization.md @@ -18,7 +18,7 @@ Both `TibberPricesSensor` and `TibberPricesBinarySensor` implement `_unrecorded_ ```python class TibberPricesSensor(TibberPricesEntity, SensorEntity): """tibber_prices Sensor class.""" - + _unrecorded_attributes = frozenset( { "description", @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - Only relevant at moment of reading - Won't be valid after some time - Similar to `entity_picture` in HA core image entities @@ -128,6 +136,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -139,6 +148,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -152,22 +162,27 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - `timestamp` - Critical for time-series analysis (ALWAYS FIRST) - All price values - Core sensor states ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 47 attributes excluded + - Class: `TibberPricesSensor` + - 47 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 30 attributes excluded + - Class: `TibberPricesBinarySensor` + - 30 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,13 +282,14 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql -SELECT +SELECT state_id, attributes -FROM states +FROM states WHERE entity_id = 'sensor.tibber_home_current_interval_price' -ORDER BY last_updated DESC +ORDER BY last_updated DESC LIMIT 5; ``` diff --git a/docs/developer/versioned_docs/version-v0.23.0/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.23.0/refactoring-guide.md index 3d0101f..fb5a2b8 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.23.0/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.0/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.0/AG ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.23.0/release-management.md b/docs/developer/versioned_docs/version-v0.23.0/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/release-management.md +++ b/docs/developer/versioned_docs/version-v0.23.0/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.23.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.23.0/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.23.0/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.23.0/setup.md b/docs/developer/versioned_docs/version-v0.23.0/setup.md index 745e534..05c4bd0 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/setup.md +++ b/docs/developer/versioned_docs/version-v0.23.0/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.23.0/testing.md b/docs/developer/versioned_docs/version-v0.23.0/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/testing.md +++ b/docs/developer/versioned_docs/version-v0.23.0/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.23.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.23.0/timer-architecture.md index c9146fd..ad9dbe4 100644 --- a/docs/developer/versioned_docs/version-v0.23.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.23.0/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.23.1/api-reference.md b/docs/developer/versioned_docs/version-v0.23.1/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.23.1/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.23.1/architecture.md b/docs/developer/versioned_docs/version-v0.23.1/architecture.md index 87fabe5..b083eb4 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/architecture.md +++ b/docs/developer/versioned_docs/version-v0.23.1/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.23.1/caching-strategy.md b/docs/developer/versioned_docs/version-v0.23.1/caching-strategy.md index 229629e..50d9208 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.23.1/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.23.1/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.23.1/coding-guidelines.md index 58a1bf1..7c6d559 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.23.1/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.23.1/contributing.md b/docs/developer/versioned_docs/version-v0.23.1/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/contributing.md +++ b/docs/developer/versioned_docs/version-v0.23.1/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.23.1/critical-patterns.md b/docs/developer/versioned_docs/version-v0.23.1/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.23.1/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.23.1/debugging.md b/docs/developer/versioned_docs/version-v0.23.1/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/debugging.md +++ b/docs/developer/versioned_docs/version-v0.23.1/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.23.1/intro.md b/docs/developer/versioned_docs/version-v0.23.1/intro.md index f8a6b02..f7f4b8d 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/intro.md +++ b/docs/developer/versioned_docs/version-v0.23.1/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.1/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.1/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.1/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.23.1/performance.md b/docs/developer/versioned_docs/version-v0.23.1/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/performance.md +++ b/docs/developer/versioned_docs/version-v0.23.1/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.23.1/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.23.1/period-calculation-theory.md index 1429540..861193c 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.23.1/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -395,6 +434,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -418,6 +458,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -427,26 +468,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -454,6 +496,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -472,12 +515,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -492,6 +537,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -502,6 +548,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -511,6 +558,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -526,43 +574,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -573,18 +623,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -598,19 +651,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -624,15 +677,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -645,52 +700,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -706,11 +770,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -724,11 +790,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -743,11 +811,13 @@ DEBUG: Day 2025-11-11: Baseline insufficient (1 < 2), starting relaxation **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -762,6 +832,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -777,6 +848,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -790,31 +862,31 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels --- @@ -823,19 +895,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -854,6 +926,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -867,11 +940,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -883,17 +958,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -906,22 +984,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -936,14 +1018,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -953,6 +1038,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -962,6 +1048,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -971,15 +1058,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -991,6 +1081,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1042,6 +1133,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1053,6 +1145,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1061,15 +1154,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1077,24 +1172,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.23.1/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.23.1/recorder-optimization.md index a40d78f..984dcc0 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.23.1/recorder-optimization.md @@ -18,7 +18,7 @@ Both `TibberPricesSensor` and `TibberPricesBinarySensor` implement `_unrecorded_ ```python class TibberPricesSensor(TibberPricesEntity, SensorEntity): """tibber_prices Sensor class.""" - + _unrecorded_attributes = frozenset( { "description", @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - Only relevant at moment of reading - Won't be valid after some time - Similar to `entity_picture` in HA core image entities @@ -128,6 +136,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -139,6 +148,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -152,22 +162,27 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - `timestamp` - Critical for time-series analysis (ALWAYS FIRST) - All price values - Core sensor states ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 47 attributes excluded + - Class: `TibberPricesSensor` + - 47 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 30 attributes excluded + - Class: `TibberPricesBinarySensor` + - 30 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,13 +282,14 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql -SELECT +SELECT state_id, attributes -FROM states +FROM states WHERE entity_id = 'sensor.tibber_home_current_interval_price' -ORDER BY last_updated DESC +ORDER BY last_updated DESC LIMIT 5; ``` diff --git a/docs/developer/versioned_docs/version-v0.23.1/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.23.1/refactoring-guide.md index 94e8f94..d974611 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.23.1/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.1/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.23.1/AG ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.23.1/release-management.md b/docs/developer/versioned_docs/version-v0.23.1/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/release-management.md +++ b/docs/developer/versioned_docs/version-v0.23.1/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.23.1/repairs-system.md b/docs/developer/versioned_docs/version-v0.23.1/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.23.1/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.23.1/setup.md b/docs/developer/versioned_docs/version-v0.23.1/setup.md index 5fad89e..bab83e6 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/setup.md +++ b/docs/developer/versioned_docs/version-v0.23.1/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.23.1/testing.md b/docs/developer/versioned_docs/version-v0.23.1/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/testing.md +++ b/docs/developer/versioned_docs/version-v0.23.1/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.23.1/timer-architecture.md b/docs/developer/versioned_docs/version-v0.23.1/timer-architecture.md index d6b2e85..bf8331e 100644 --- a/docs/developer/versioned_docs/version-v0.23.1/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.23.1/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.24.0/api-reference.md b/docs/developer/versioned_docs/version-v0.24.0/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.24.0/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.24.0/architecture.md b/docs/developer/versioned_docs/version-v0.24.0/architecture.md index 43e1817..9dd49d8 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.24.0/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.24.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.24.0/caching-strategy.md index b2d48a8..c296585 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.24.0/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.24.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.24.0/coding-guidelines.md index c062bcb..c4241d7 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.24.0/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.24.0/contributing.md b/docs/developer/versioned_docs/version-v0.24.0/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/contributing.md +++ b/docs/developer/versioned_docs/version-v0.24.0/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.24.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.24.0/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.24.0/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.24.0/debugging.md b/docs/developer/versioned_docs/version-v0.24.0/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.24.0/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.24.0/intro.md b/docs/developer/versioned_docs/version-v0.24.0/intro.md index 8c1af08..f5e2832 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/intro.md +++ b/docs/developer/versioned_docs/version-v0.24.0/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.24.0/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.24.0/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.24.0/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.24.0/performance.md b/docs/developer/versioned_docs/version-v0.24.0/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.24.0/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.24.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.24.0/period-calculation-theory.md index d3d5dbe..b5d8cc1 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.24.0/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -395,6 +434,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -418,6 +458,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -427,26 +468,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -454,6 +496,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -472,12 +515,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -492,6 +537,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -502,6 +548,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -511,6 +558,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -526,43 +574,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -573,18 +623,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -598,19 +651,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -624,15 +677,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -645,52 +700,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -706,11 +770,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -724,11 +790,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -743,11 +811,13 @@ DEBUG: Day 2025-11-11: Baseline insufficient (1 < 2), starting relaxation **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -762,6 +832,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -777,6 +848,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -790,31 +862,31 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels --- @@ -823,19 +895,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -854,6 +926,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -867,11 +940,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -883,17 +958,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -906,22 +984,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -936,14 +1018,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -953,6 +1038,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -962,6 +1048,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -971,15 +1058,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -991,6 +1081,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1042,6 +1133,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1053,6 +1145,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1061,15 +1154,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1077,24 +1172,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.24.0/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.24.0/recorder-optimization.md index a40d78f..984dcc0 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.24.0/recorder-optimization.md @@ -18,7 +18,7 @@ Both `TibberPricesSensor` and `TibberPricesBinarySensor` implement `_unrecorded_ ```python class TibberPricesSensor(TibberPricesEntity, SensorEntity): """tibber_prices Sensor class.""" - + _unrecorded_attributes = frozenset( { "description", @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - Only relevant at moment of reading - Won't be valid after some time - Similar to `entity_picture` in HA core image entities @@ -128,6 +136,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -139,6 +148,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -152,22 +162,27 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - `timestamp` - Critical for time-series analysis (ALWAYS FIRST) - All price values - Core sensor states ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 47 attributes excluded + - Class: `TibberPricesSensor` + - 47 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 30 attributes excluded + - Class: `TibberPricesBinarySensor` + - 30 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,13 +282,14 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql -SELECT +SELECT state_id, attributes -FROM states +FROM states WHERE entity_id = 'sensor.tibber_home_current_interval_price' -ORDER BY last_updated DESC +ORDER BY last_updated DESC LIMIT 5; ``` diff --git a/docs/developer/versioned_docs/version-v0.24.0/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.24.0/refactoring-guide.md index 5499e26..9d35e6d 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.24.0/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.24.0/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.24.0/AG ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.24.0/release-management.md b/docs/developer/versioned_docs/version-v0.24.0/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/release-management.md +++ b/docs/developer/versioned_docs/version-v0.24.0/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.24.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.24.0/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.24.0/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.24.0/setup.md b/docs/developer/versioned_docs/version-v0.24.0/setup.md index b95e5a2..3b8ada7 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/setup.md +++ b/docs/developer/versioned_docs/version-v0.24.0/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.24.0/testing.md b/docs/developer/versioned_docs/version-v0.24.0/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/testing.md +++ b/docs/developer/versioned_docs/version-v0.24.0/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.24.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.24.0/timer-architecture.md index d9ad125..cc6547c 100644 --- a/docs/developer/versioned_docs/version-v0.24.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.24.0/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.27.0/api-reference.md b/docs/developer/versioned_docs/version-v0.27.0/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.27.0/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.27.0/architecture.md b/docs/developer/versioned_docs/version-v0.27.0/architecture.md index e430325..84b0911 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.27.0/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.27.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.27.0/caching-strategy.md index 08bd277..29923f9 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.27.0/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.27.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.27.0/coding-guidelines.md index 5721e08..cd70a8c 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.27.0/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.27.0/contributing.md b/docs/developer/versioned_docs/version-v0.27.0/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/contributing.md +++ b/docs/developer/versioned_docs/version-v0.27.0/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.27.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.27.0/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.27.0/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.27.0/debugging.md b/docs/developer/versioned_docs/version-v0.27.0/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.27.0/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.27.0/intro.md b/docs/developer/versioned_docs/version-v0.27.0/intro.md index 004b0a0..f0075f1 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/intro.md +++ b/docs/developer/versioned_docs/version-v0.27.0/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.27.0/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.27.0/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.27.0/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.27.0/performance.md b/docs/developer/versioned_docs/version-v0.27.0/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.27.0/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.27.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.27.0/period-calculation-theory.md index 194904a..f032cc0 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.27.0/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -395,6 +434,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -418,6 +458,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -427,26 +468,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -454,6 +496,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -472,12 +515,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -492,6 +537,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -502,6 +548,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -511,6 +558,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -526,43 +574,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -573,18 +623,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -598,19 +651,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -624,15 +677,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -645,52 +700,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -706,11 +770,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -724,11 +790,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -743,11 +811,13 @@ DEBUG: Day 2025-11-11: Baseline insufficient (1 < 2), starting relaxation **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -762,6 +832,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -777,6 +848,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -790,31 +862,31 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels --- @@ -823,19 +895,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -854,6 +926,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -867,11 +940,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -883,17 +958,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -906,22 +984,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -936,14 +1018,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -953,6 +1038,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -962,6 +1048,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -971,15 +1058,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -991,6 +1081,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1042,6 +1133,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1053,6 +1145,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1061,15 +1154,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1077,24 +1172,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.27.0/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.27.0/recorder-optimization.md index a40d78f..984dcc0 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.27.0/recorder-optimization.md @@ -18,7 +18,7 @@ Both `TibberPricesSensor` and `TibberPricesBinarySensor` implement `_unrecorded_ ```python class TibberPricesSensor(TibberPricesEntity, SensorEntity): """tibber_prices Sensor class.""" - + _unrecorded_attributes = frozenset( { "description", @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - Only relevant at moment of reading - Won't be valid after some time - Similar to `entity_picture` in HA core image entities @@ -128,6 +136,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -139,6 +148,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -152,22 +162,27 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - `timestamp` - Critical for time-series analysis (ALWAYS FIRST) - All price values - Core sensor states ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 47 attributes excluded + - Class: `TibberPricesSensor` + - 47 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 30 attributes excluded + - Class: `TibberPricesBinarySensor` + - 30 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,13 +282,14 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql -SELECT +SELECT state_id, attributes -FROM states +FROM states WHERE entity_id = 'sensor.tibber_home_current_interval_price' -ORDER BY last_updated DESC +ORDER BY last_updated DESC LIMIT 5; ``` diff --git a/docs/developer/versioned_docs/version-v0.27.0/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.27.0/refactoring-guide.md index d0494d2..4f286d9 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.27.0/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.27.0/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.27.0/AG ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.27.0/release-management.md b/docs/developer/versioned_docs/version-v0.27.0/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/release-management.md +++ b/docs/developer/versioned_docs/version-v0.27.0/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.27.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.27.0/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.27.0/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.27.0/setup.md b/docs/developer/versioned_docs/version-v0.27.0/setup.md index aa405e7..a6f4542 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/setup.md +++ b/docs/developer/versioned_docs/version-v0.27.0/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.27.0/testing.md b/docs/developer/versioned_docs/version-v0.27.0/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/testing.md +++ b/docs/developer/versioned_docs/version-v0.27.0/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.27.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.27.0/timer-architecture.md index 7ebcabb..60c9b22 100644 --- a/docs/developer/versioned_docs/version-v0.27.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.27.0/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.28.0/api-reference.md b/docs/developer/versioned_docs/version-v0.28.0/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.28.0/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.28.0/architecture.md b/docs/developer/versioned_docs/version-v0.28.0/architecture.md index 03dd073..d7ed0d1 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.28.0/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.28.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.28.0/caching-strategy.md index 17466c3..68023f0 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.28.0/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.28.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.28.0/coding-guidelines.md index 340a754..873c893 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.28.0/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.28.0/contributing.md b/docs/developer/versioned_docs/version-v0.28.0/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/contributing.md +++ b/docs/developer/versioned_docs/version-v0.28.0/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.28.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.28.0/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.28.0/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.28.0/debugging.md b/docs/developer/versioned_docs/version-v0.28.0/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.28.0/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.28.0/intro.md b/docs/developer/versioned_docs/version-v0.28.0/intro.md index 79e2fe8..e78b117 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/intro.md +++ b/docs/developer/versioned_docs/version-v0.28.0/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.28.0/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.28.0/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.28.0/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.28.0/performance.md b/docs/developer/versioned_docs/version-v0.28.0/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.28.0/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.28.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.28.0/period-calculation-theory.md index 692329c..dda7ad9 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.28.0/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -397,6 +436,7 @@ These three mechanisms handle pathological price situations where standard filte **Problem:** When all prices are nearly identical (e.g. 28–32 ct, CV=5.4%), requiring 2 distinct "best price" windows is geometrically impossible. Even after exhausting all 11 relaxation phases, only 1 period exists because there is no second cheap cluster. **Solution:** Before the baseline counting loop, compute per-day effective min_periods: + ```python if day_cv <= 10%: day_effective_min[day] = 1 # Flat day: 1 period is enough @@ -417,17 +457,18 @@ else: **Problem:** On solar surplus days (avg 2–5 ct/kWh), a percentage-based min_distance like 5% means only 0.1 ct absolute separation is required. The filter either accepts almost the entire day (if ref_price is 2 ct, 5% = 0.1 ct – nearly everything qualifies) or blocks everything (if the spread is within that 0.1 ct band). **Solution:** Linear scaling toward zero as avg_price approaches zero: + ``` scale_factor = avg_price / LOW_PRICE_AVG_THRESHOLD adjusted_min_distance = original_min_distance × scale_factor ``` -| avg_price | scale | Effect on 5% min_distance | -|---|---|---| -| ≥ 10 ct (0.10 EUR) | 100% | 5% (full distance) | -| 5 ct (0.05 EUR) | 50% | 2.5% | -| 2 ct (0.02 EUR) | 20% | 1% | -| 0 ct | 0% | 0% (disabled) | +| avg_price | scale | Effect on 5% min_distance | +| ------------------ | ----- | ------------------------- | +| ≥ 10 ct (0.10 EUR) | 100% | 5% (full distance) | +| 5 ct (0.05 EUR) | 50% | 2.5% | +| 2 ct (0.02 EUR) | 20% | 1% | +| 0 ct | 0% | 0% (disabled) | ### 3. CV Quality Gate Bypass for Absolute Low-Price Periods @@ -435,11 +476,12 @@ adjusted_min_distance = original_min_distance × scale_factor **Trigger:** Period mean price < `LOW_PRICE_QUALITY_BYPASS_THRESHOLD` (0.10 EUR) -**Problem:** A period at 0.5–4 ct has high *relative* variation (CV ≈ 70–80%), but the absolute differences are fractions of a cent. The quality gate (CV ≤ `PERIOD_MAX_CV`) with a relative metric would wrongly reject this as a "heterogeneous" period. +**Problem:** A period at 0.5–4 ct has high _relative_ variation (CV ≈ 70–80%), but the absolute differences are fractions of a cent. The quality gate (CV ≤ `PERIOD_MAX_CV`) with a relative metric would wrongly reject this as a "heterogeneous" period. **Distinguishes from flat normal days:** A flat day at 33–36 ct also has low absolute range, but mean is 34.5 ct (>> 0.10 EUR threshold). The bypass only applies when the mean itself is below the threshold – i.e. the day is genuinely cheap in absolute terms. **Solution:** Short-circuit the quality gate check: + ```python period_mean = sum(period_prices) / len(period_prices) if period_mean < LOW_PRICE_QUALITY_BYPASS_THRESHOLD: @@ -470,6 +512,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -493,6 +536,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -502,26 +546,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -529,6 +574,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -547,12 +593,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -567,6 +615,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -577,6 +626,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -586,6 +636,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -601,43 +652,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -648,18 +701,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -673,19 +729,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -699,15 +755,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -720,52 +778,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -781,11 +848,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -799,11 +868,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -825,6 +896,7 @@ Relaxation would exhaust all 11 phases trying to find a second period. All price `_compute_day_effective_min()` detects CV ≤ 10% and sets `day_effective_min = 1` for this day. The result is accepted after finding the single cheapest cluster. **Expected Logs:** + ``` DEBUG: Day 2025-11-11: flat price profile (CV=5.4% ≤ 10.0%) → min_periods relaxed to 1 INFO: Adaptive min_periods: 1 flat day(s) (CV ≤ 10%) need only 1 period instead of 2 @@ -832,10 +904,11 @@ INFO: Day 2025-11-11: Baseline satisfied (1 period, effective minimum is 1) ``` **Sensor Attributes:** + ```yaml -min_periods_configured: 2 # User's setting -periods_found_total: 1 # Actual result -flat_days_detected: 1 # Explains the difference +min_periods_configured: 2 # User's setting +periods_found_total: 1 # Actual result +flat_days_detected: 1 # Explains the difference ``` **Why not for Peak Price?** @@ -848,18 +921,20 @@ Peak price always runs full relaxation. On a flat day, the integration still nee **Configuration:** `min_periods_best: 2`, 5% min_distance **Problems without fixes:** -1. **min_distance conflict:** 5% of 2.1 ct = 0.105 ct minimum distance. Only prices ≤ 1.995 ct qualify. The daily minimum is 0.5 ct – well within range. But the *relative* threshold becomes meaninglessly tiny: the entire day could qualify. + +1. **min_distance conflict:** 5% of 2.1 ct = 0.105 ct minimum distance. Only prices ≤ 1.995 ct qualify. The daily minimum is 0.5 ct – well within range. But the _relative_ threshold becomes meaninglessly tiny: the entire day could qualify. 2. **CV quality gate:** Prices 0.5–4.2 ct show high relative variation (CV ≈ 70-80%), but the absolute differences are fractions of a cent. The quality gate would wrongly reject valid periods. **Implemented behavior:** -*`LOW_PRICE_AVG_THRESHOLD = 0.10 EUR` (level_filtering.py):* +_`LOW_PRICE_AVG_THRESHOLD = 0.10 EUR` (level_filtering.py):_ When `avg_price < 0.10 EUR`, min_distance is scaled linearly to 0. At avg=2.1 ct (0.021 EUR), scale ≈ 21% → min_distance effectively 1%. Prevents the distance filter from blocking the entire day or accepting the entire day. -*`LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 EUR` (relaxation.py):* +_`LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 EUR` (relaxation.py):_ When period mean < 0.10 EUR, the CV quality gate is bypassed entirely. A period at 0.5–2 ct with CV=60% is practically homogeneous from a cost perspective. **Expected Logs:** + ``` DEBUG: Low-price day (avg=0.021 EUR < 0.10 threshold): min_distance scaled 5% → 1.1% DEBUG: Period 02:00-05:00: mean=0.009 EUR < bypass threshold → quality gate bypassed @@ -871,11 +946,13 @@ DEBUG: Period 02:00-05:00: mean=0.009 EUR < bypass threshold → quality gate **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -890,6 +967,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -905,6 +983,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -918,50 +997,50 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels 6. **Check Flat Day / Low-Price Adaptations** - - Sensor shows `flat_days_detected: N` → CV ≤ 10%, adaptive min_periods reduced target to 1 - - Sensor shows `relaxation_incomplete: true` without `flat_days_detected` → check filter settings - - Low absolute prices (avg < 10 ct): min_distance is auto-scaled, CV quality gate is bypassed - - To confirm: Enable DEBUG logging for `custom_components.tibber_prices.coordinator.period_handlers.relaxation.details` - - Look for: `"flat price profile (CV=X.X% ≤ 10.0%) → min_periods relaxed to 1"` - - Look for: `"Low-price day (avg=0.0XX EUR < 0.10 threshold): min_distance scaled X% → Y%"` + - Sensor shows `flat_days_detected: N` → CV ≤ 10%, adaptive min_periods reduced target to 1 + - Sensor shows `relaxation_incomplete: true` without `flat_days_detected` → check filter settings + - Low absolute prices (avg < 10 ct): min_distance is auto-scaled, CV quality gate is bypassed + - To confirm: Enable DEBUG logging for `custom_components.tibber_prices.coordinator.period_handlers.relaxation.details` + - Look for: `"flat price profile (CV=X.X% ≤ 10.0%) → min_periods relaxed to 1"` + - Look for: `"Low-price day (avg=0.0XX EUR < 0.10 threshold): min_distance scaled X% → Y%"` **Diagnostic Sensor Attributes Summary:** -| Attribute | Type | When shown | Meaning | -|---|---|---|---| -| `min_periods_configured` | int | Always | User's configured target per day | -| `periods_found_total` | int | Always | Actual periods found across all days | -| `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | -| `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | -| `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | -| `relaxation_level` | string | Only when active | Flex% and filter combo that succeeded | +| Attribute | Type | When shown | Meaning | +| ------------------------ | ------ | ---------------- | ------------------------------------------- | +| `min_periods_configured` | int | Always | User's configured target per day | +| `periods_found_total` | int | Always | Actual periods found across all days | +| `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | +| `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | +| `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | +| `relaxation_level` | string | Only when active | Flex% and filter combo that succeeded | --- @@ -970,19 +1049,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -1001,6 +1080,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -1014,11 +1094,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -1030,17 +1112,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -1053,22 +1138,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -1083,14 +1172,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -1100,6 +1192,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -1109,6 +1202,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -1118,15 +1212,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -1138,6 +1235,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1189,6 +1287,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1200,6 +1299,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1208,15 +1308,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1224,24 +1326,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.28.0/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.28.0/recorder-optimization.md index 50cc494..f56ea9c 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.28.0/recorder-optimization.md @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `timestamp`, `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - `timestamp` is the rounded-quarter reference time used at the moment of the state write — it's stale as soon as the next update fires and has no analytical value in history - `next_api_poll`, `next_midnight_turnover` etc. are only relevant at the moment of reading; they're superseded by the next update - Similar to `entity_picture` in HA core image entities @@ -129,6 +137,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -140,6 +149,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -153,21 +163,26 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - All price values - Core sensor states (the entity's `native_value` is always recorded separately) ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 46 attributes excluded + - Class: `TibberPricesSensor` + - 46 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 29 attributes excluded + - Class: `TibberPricesBinarySensor` + - 29 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,6 +282,7 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql SELECT state_id, @@ -298,11 +316,11 @@ This makes `state_class=TOTAL` on many sensors the primary cause of long-term da For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_class` values are valid: -| `state_class` | Statistics written | Frontend effect | -|---|---|---| -| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page | -| `None` | ❌ No | States timeline only (History panel, "Show More") | -| `MEASUREMENT` | ❌ Blocked by hassfest | — | +| `state_class` | Statistics written | Frontend effect | +| ------------- | ------------------------- | ------------------------------------------------- | +| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page | +| `None` | ❌ No | States timeline only (History panel, "Show More") | +| `MEASUREMENT` | ❌ Blocked by hassfest | — | `MEASUREMENT` causes a hassfest validation error for MONETARY sensors, leaving only `TOTAL` or `None`. @@ -310,19 +328,21 @@ For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_clas Only 3 of 26 MONETARY sensors keep `state_class=TOTAL` — those where long-term history is genuinely useful: -| Sensor | Reason | -|---|---| -| `current_interval_price` | Long-term price trend (weeks/months) | -| `current_interval_price_base` | Required for Energy Dashboard | -| `average_price_today` | Seasonal daily average tracking | +| Sensor | Reason | +| ----------------------------- | ------------------------------------ | +| `current_interval_price` | Long-term price trend (weeks/months) | +| `current_interval_price_base` | Required for Energy Dashboard | +| `average_price_today` | Seasonal daily average tracking | All other 23 MONETARY sensors use `state_class=None`: + - Forecast/future sensors (`next_avg_*h`) - Daily snapshots (`lowest/highest_price_today/tomorrow`) - Rolling windows (`trailing/leading_24h_*`) - Next/previous interval sensors **Effect of `state_class=None`:** + - ✅ Short-term state history (States timeline, ~10 days) still works normally - ✅ Templates, automations, and attributes are unaffected - ❌ Statistics line-chart removed from entity detail page for these sensors @@ -331,6 +351,7 @@ All other 23 MONETARY sensors use `state_class=None`: ### Expected Impact Going from 26 → 3 sensors writing to the statistics tables: + - **~88% reduction** in statistics table writes - Prevents the primary cause of long-term database bloat - Existing statistics data is retained (only new writes stop) @@ -339,10 +360,10 @@ Going from 26 → 3 sensors writing to the statistics tables: These are two independent mechanisms targeting different tables: -| Mechanism | Table affected | Purged? | Controls | -|---|---|---|---| -| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write | -| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all | +| Mechanism | Table affected | Purged? | Controls | +| ------------------------ | ------------------------------------- | ----------- | ----------------------------------------------- | +| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write | +| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all | Both optimizations work together. `_unrecorded_attributes` reduces the size of each state write; `state_class=None` eliminates an entire category of unbounded writes. diff --git a/docs/developer/versioned_docs/version-v0.28.0/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.28.0/refactoring-guide.md index 7644da8..3276138 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.28.0/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.28.0/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.28.0/AG ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.28.0/release-management.md b/docs/developer/versioned_docs/version-v0.28.0/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/release-management.md +++ b/docs/developer/versioned_docs/version-v0.28.0/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.28.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.28.0/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.28.0/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.28.0/setup.md b/docs/developer/versioned_docs/version-v0.28.0/setup.md index c36ea0c..2598ff3 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/setup.md +++ b/docs/developer/versioned_docs/version-v0.28.0/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.28.0/testing.md b/docs/developer/versioned_docs/version-v0.28.0/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/testing.md +++ b/docs/developer/versioned_docs/version-v0.28.0/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.28.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.28.0/timer-architecture.md index 7336adf..d9a88cb 100644 --- a/docs/developer/versioned_docs/version-v0.28.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.28.0/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.29.0/api-reference.md b/docs/developer/versioned_docs/version-v0.29.0/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.29.0/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.29.0/architecture.md b/docs/developer/versioned_docs/version-v0.29.0/architecture.md index 1aca29b..ddc5cb2 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.29.0/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.29.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.29.0/caching-strategy.md index f25277c..8c23138 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.29.0/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.29.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.29.0/coding-guidelines.md index 56abdf4..13f7adb 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.29.0/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.29.0/contributing.md b/docs/developer/versioned_docs/version-v0.29.0/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/contributing.md +++ b/docs/developer/versioned_docs/version-v0.29.0/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.29.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.29.0/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.29.0/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.29.0/debugging.md b/docs/developer/versioned_docs/version-v0.29.0/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.29.0/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.29.0/intro.md b/docs/developer/versioned_docs/version-v0.29.0/intro.md index 0af1897..e79f69b 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/intro.md +++ b/docs/developer/versioned_docs/version-v0.29.0/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.29.0/performance.md b/docs/developer/versioned_docs/version-v0.29.0/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.29.0/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.29.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.29.0/period-calculation-theory.md index b2bbef8..482090b 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.29.0/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -397,6 +436,7 @@ These three mechanisms handle pathological price situations where standard filte **Problem:** When all prices are nearly identical (e.g. 28–32 ct, CV=5.4%), requiring 2 distinct "best price" windows is geometrically impossible. Even after exhausting all 11 relaxation phases, only 1 period exists because there is no second cheap cluster. **Solution:** Before the baseline counting loop, compute per-day effective min_periods: + ```python if day_cv <= 10%: day_effective_min[day] = 1 # Flat day: 1 period is enough @@ -417,17 +457,18 @@ else: **Problem:** On solar surplus days (avg 2–5 ct/kWh), a percentage-based min_distance like 5% means only 0.1 ct absolute separation is required. The filter either accepts almost the entire day (if ref_price is 2 ct, 5% = 0.1 ct – nearly everything qualifies) or blocks everything (if the spread is within that 0.1 ct band). **Solution:** Linear scaling toward zero as avg_price approaches zero: + ``` scale_factor = avg_price / LOW_PRICE_AVG_THRESHOLD adjusted_min_distance = original_min_distance × scale_factor ``` -| avg_price | scale | Effect on 5% min_distance | -|---|---|---| -| ≥ 10 ct (0.10 EUR) | 100% | 5% (full distance) | -| 5 ct (0.05 EUR) | 50% | 2.5% | -| 2 ct (0.02 EUR) | 20% | 1% | -| 0 ct | 0% | 0% (disabled) | +| avg_price | scale | Effect on 5% min_distance | +| ------------------ | ----- | ------------------------- | +| ≥ 10 ct (0.10 EUR) | 100% | 5% (full distance) | +| 5 ct (0.05 EUR) | 50% | 2.5% | +| 2 ct (0.02 EUR) | 20% | 1% | +| 0 ct | 0% | 0% (disabled) | ### 3. CV Quality Gate Bypass for Absolute Low-Price Periods @@ -435,11 +476,12 @@ adjusted_min_distance = original_min_distance × scale_factor **Trigger:** Period mean price < `LOW_PRICE_QUALITY_BYPASS_THRESHOLD` (0.10 EUR) -**Problem:** A period at 0.5–4 ct has high *relative* variation (CV ≈ 70–80%), but the absolute differences are fractions of a cent. The quality gate (CV ≤ `PERIOD_MAX_CV`) with a relative metric would wrongly reject this as a "heterogeneous" period. +**Problem:** A period at 0.5–4 ct has high _relative_ variation (CV ≈ 70–80%), but the absolute differences are fractions of a cent. The quality gate (CV ≤ `PERIOD_MAX_CV`) with a relative metric would wrongly reject this as a "heterogeneous" period. **Distinguishes from flat normal days:** A flat day at 33–36 ct also has low absolute range, but mean is 34.5 ct (>> 0.10 EUR threshold). The bypass only applies when the mean itself is below the threshold – i.e. the day is genuinely cheap in absolute terms. **Solution:** Short-circuit the quality gate check: + ```python period_mean = sum(period_prices) / len(period_prices) if period_mean < LOW_PRICE_QUALITY_BYPASS_THRESHOLD: @@ -470,6 +512,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -493,6 +536,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -502,26 +546,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -529,6 +574,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -547,12 +593,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -567,6 +615,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -577,6 +626,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -586,6 +636,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -601,43 +652,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -648,18 +701,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -673,19 +729,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -699,15 +755,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -720,52 +778,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -781,11 +848,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -799,11 +868,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -825,6 +896,7 @@ Relaxation would exhaust all 11 phases trying to find a second period. All price `_compute_day_effective_min()` detects CV ≤ 10% and sets `day_effective_min = 1` for this day. The result is accepted after finding the single cheapest cluster. **Expected Logs:** + ``` DEBUG: Day 2025-11-11: flat price profile (CV=5.4% ≤ 10.0%) → min_periods relaxed to 1 INFO: Adaptive min_periods: 1 flat day(s) (CV ≤ 10%) need only 1 period instead of 2 @@ -832,10 +904,11 @@ INFO: Day 2025-11-11: Baseline satisfied (1 period, effective minimum is 1) ``` **Sensor Attributes:** + ```yaml -min_periods_configured: 2 # User's setting -periods_found_total: 1 # Actual result -flat_days_detected: 1 # Explains the difference +min_periods_configured: 2 # User's setting +periods_found_total: 1 # Actual result +flat_days_detected: 1 # Explains the difference ``` **Why not for Peak Price?** @@ -848,18 +921,20 @@ Peak price always runs full relaxation. On a flat day, the integration still nee **Configuration:** `min_periods_best: 2`, 5% min_distance **Problems without fixes:** -1. **min_distance conflict:** 5% of 2.1 ct = 0.105 ct minimum distance. Only prices ≤ 1.995 ct qualify. The daily minimum is 0.5 ct – well within range. But the *relative* threshold becomes meaninglessly tiny: the entire day could qualify. + +1. **min_distance conflict:** 5% of 2.1 ct = 0.105 ct minimum distance. Only prices ≤ 1.995 ct qualify. The daily minimum is 0.5 ct – well within range. But the _relative_ threshold becomes meaninglessly tiny: the entire day could qualify. 2. **CV quality gate:** Prices 0.5–4.2 ct show high relative variation (CV ≈ 70-80%), but the absolute differences are fractions of a cent. The quality gate would wrongly reject valid periods. **Implemented behavior:** -*`LOW_PRICE_AVG_THRESHOLD = 0.10 EUR` (level_filtering.py):* +_`LOW_PRICE_AVG_THRESHOLD = 0.10 EUR` (level_filtering.py):_ When `avg_price < 0.10 EUR`, min_distance is scaled linearly to 0. At avg=2.1 ct (0.021 EUR), scale ≈ 21% → min_distance effectively 1%. Prevents the distance filter from blocking the entire day or accepting the entire day. -*`LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 EUR` (relaxation.py):* +_`LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 EUR` (relaxation.py):_ When period mean < 0.10 EUR, the CV quality gate is bypassed entirely. A period at 0.5–2 ct with CV=60% is practically homogeneous from a cost perspective. **Expected Logs:** + ``` DEBUG: Low-price day (avg=0.021 EUR < 0.10 threshold): min_distance scaled 5% → 1.1% DEBUG: Period 02:00-05:00: mean=0.009 EUR < bypass threshold → quality gate bypassed @@ -871,11 +946,13 @@ DEBUG: Period 02:00-05:00: mean=0.009 EUR < bypass threshold → quality gate **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -890,6 +967,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -905,6 +983,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -918,50 +997,50 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels 6. **Check Flat Day / Low-Price Adaptations** - - Sensor shows `flat_days_detected: N` → CV ≤ 10%, adaptive min_periods reduced target to 1 - - Sensor shows `relaxation_incomplete: true` without `flat_days_detected` → check filter settings - - Low absolute prices (avg < 10 ct): min_distance is auto-scaled, CV quality gate is bypassed - - To confirm: Enable DEBUG logging for `custom_components.tibber_prices.coordinator.period_handlers.relaxation.details` - - Look for: `"flat price profile (CV=X.X% ≤ 10.0%) → min_periods relaxed to 1"` - - Look for: `"Low-price day (avg=0.0XX EUR < 0.10 threshold): min_distance scaled X% → Y%"` + - Sensor shows `flat_days_detected: N` → CV ≤ 10%, adaptive min_periods reduced target to 1 + - Sensor shows `relaxation_incomplete: true` without `flat_days_detected` → check filter settings + - Low absolute prices (avg < 10 ct): min_distance is auto-scaled, CV quality gate is bypassed + - To confirm: Enable DEBUG logging for `custom_components.tibber_prices.coordinator.period_handlers.relaxation.details` + - Look for: `"flat price profile (CV=X.X% ≤ 10.0%) → min_periods relaxed to 1"` + - Look for: `"Low-price day (avg=0.0XX EUR < 0.10 threshold): min_distance scaled X% → Y%"` **Diagnostic Sensor Attributes Summary:** -| Attribute | Type | When shown | Meaning | -|---|---|---|---| -| `min_periods_configured` | int | Always | User's configured target per day | -| `periods_found_total` | int | Always | Actual periods found across all days | -| `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | -| `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | -| `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | -| `relaxation_level` | string | Only when active | Flex% and filter combo that succeeded | +| Attribute | Type | When shown | Meaning | +| ------------------------ | ------ | ---------------- | ------------------------------------------- | +| `min_periods_configured` | int | Always | User's configured target per day | +| `periods_found_total` | int | Always | Actual periods found across all days | +| `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | +| `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | +| `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | +| `relaxation_level` | string | Only when active | Flex% and filter combo that succeeded | --- @@ -970,19 +1049,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -1001,6 +1080,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -1014,11 +1094,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -1030,17 +1112,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -1053,22 +1138,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -1083,14 +1172,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -1100,6 +1192,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -1109,6 +1202,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -1118,15 +1212,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -1138,6 +1235,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1189,6 +1287,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1200,6 +1299,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1208,15 +1308,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1224,24 +1326,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.29.0/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.29.0/recorder-optimization.md index 50cc494..f56ea9c 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.29.0/recorder-optimization.md @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `timestamp`, `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - `timestamp` is the rounded-quarter reference time used at the moment of the state write — it's stale as soon as the next update fires and has no analytical value in history - `next_api_poll`, `next_midnight_turnover` etc. are only relevant at the moment of reading; they're superseded by the next update - Similar to `entity_picture` in HA core image entities @@ -129,6 +137,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -140,6 +149,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -153,21 +163,26 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - All price values - Core sensor states (the entity's `native_value` is always recorded separately) ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 46 attributes excluded + - Class: `TibberPricesSensor` + - 46 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 29 attributes excluded + - Class: `TibberPricesBinarySensor` + - 29 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,6 +282,7 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql SELECT state_id, @@ -298,11 +316,11 @@ This makes `state_class=TOTAL` on many sensors the primary cause of long-term da For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_class` values are valid: -| `state_class` | Statistics written | Frontend effect | -|---|---|---| -| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page | -| `None` | ❌ No | States timeline only (History panel, "Show More") | -| `MEASUREMENT` | ❌ Blocked by hassfest | — | +| `state_class` | Statistics written | Frontend effect | +| ------------- | ------------------------- | ------------------------------------------------- | +| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page | +| `None` | ❌ No | States timeline only (History panel, "Show More") | +| `MEASUREMENT` | ❌ Blocked by hassfest | — | `MEASUREMENT` causes a hassfest validation error for MONETARY sensors, leaving only `TOTAL` or `None`. @@ -310,19 +328,21 @@ For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_clas Only 3 of 26 MONETARY sensors keep `state_class=TOTAL` — those where long-term history is genuinely useful: -| Sensor | Reason | -|---|---| -| `current_interval_price` | Long-term price trend (weeks/months) | -| `current_interval_price_base` | Required for Energy Dashboard | -| `average_price_today` | Seasonal daily average tracking | +| Sensor | Reason | +| ----------------------------- | ------------------------------------ | +| `current_interval_price` | Long-term price trend (weeks/months) | +| `current_interval_price_base` | Required for Energy Dashboard | +| `average_price_today` | Seasonal daily average tracking | All other 23 MONETARY sensors use `state_class=None`: + - Forecast/future sensors (`next_avg_*h`) - Daily snapshots (`lowest/highest_price_today/tomorrow`) - Rolling windows (`trailing/leading_24h_*`) - Next/previous interval sensors **Effect of `state_class=None`:** + - ✅ Short-term state history (States timeline, ~10 days) still works normally - ✅ Templates, automations, and attributes are unaffected - ❌ Statistics line-chart removed from entity detail page for these sensors @@ -331,6 +351,7 @@ All other 23 MONETARY sensors use `state_class=None`: ### Expected Impact Going from 26 → 3 sensors writing to the statistics tables: + - **~88% reduction** in statistics table writes - Prevents the primary cause of long-term database bloat - Existing statistics data is retained (only new writes stop) @@ -339,10 +360,10 @@ Going from 26 → 3 sensors writing to the statistics tables: These are two independent mechanisms targeting different tables: -| Mechanism | Table affected | Purged? | Controls | -|---|---|---|---| -| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write | -| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all | +| Mechanism | Table affected | Purged? | Controls | +| ------------------------ | ------------------------------------- | ----------- | ----------------------------------------------- | +| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write | +| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all | Both optimizations work together. `_unrecorded_attributes` reduces the size of each state write; `state_class=None` eliminates an entire category of unbounded writes. diff --git a/docs/developer/versioned_docs/version-v0.29.0/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.29.0/refactoring-guide.md index 2b1c5fd..5babd9b 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.29.0/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AG ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.29.0/release-management.md b/docs/developer/versioned_docs/version-v0.29.0/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/release-management.md +++ b/docs/developer/versioned_docs/version-v0.29.0/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.29.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.29.0/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.29.0/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.29.0/setup.md b/docs/developer/versioned_docs/version-v0.29.0/setup.md index 7171c07..71c943f 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/setup.md +++ b/docs/developer/versioned_docs/version-v0.29.0/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.29.0/testing.md b/docs/developer/versioned_docs/version-v0.29.0/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/testing.md +++ b/docs/developer/versioned_docs/version-v0.29.0/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.29.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.29.0/timer-architecture.md index 289e926..4d2354c 100644 --- a/docs/developer/versioned_docs/version-v0.29.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.29.0/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_docs/version-v0.30.0/api-reference.md b/docs/developer/versioned_docs/version-v0.30.0/api-reference.md index 1aab789..1195b6a 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/api-reference.md +++ b/docs/developer/versioned_docs/version-v0.30.0/api-reference.md @@ -22,30 +22,30 @@ Fetches home information and metadata: ```graphql query { - viewer { - homes { - id - appNickname - address { - address1 - postalCode - city - country - } - timeZone - currentSubscription { - priceInfo { - current { - currency - } + viewer { + homes { + id + appNickname + address { + address1 + postalCode + city + country + } + timeZone + currentSubscription { + priceInfo { + current { + currency + } + } + } + meteringPointData { + consumptionEan + gridAreaCode + } } - } - meteringPointData { - consumptionEan - gridAreaCode - } } - } } ``` @@ -56,26 +56,27 @@ query { Fetches quarter-hourly prices: ```graphql -query($homeId: ID!) { - viewer { - home(id: $homeId) { - currentSubscription { - priceInfo { - range(resolution: QUARTER_HOURLY, first: 384) { - nodes { - total - startsAt - level +query ($homeId: ID!) { + viewer { + home(id: $homeId) { + currentSubscription { + priceInfo { + range(resolution: QUARTER_HOURLY, first: 384) { + nodes { + total + startsAt + level + } + } + } } - } } - } } - } } ``` **Parameters:** + - `homeId`: Tibber home identifier - `resolution`: Always `QUARTER_HOURLY` - `first`: 384 intervals (4 days of data) @@ -85,10 +86,12 @@ query($homeId: ID!) { ## Rate Limits Tibber API rate limits (as of 2024): + - **5000 requests per hour** per token - **Burst limit:** 100 requests per minute Integration stays well below these limits: + - Polls every 15 minutes = 96 requests/day - User data cached for 24h = 1 request/day - **Total:** ~100 requests/day per home @@ -99,13 +102,14 @@ Integration stays well below these limits: ```json { - "total": 0.2456, - "startsAt": "2024-12-06T14:00:00.000+01:00", - "level": "NORMAL" + "total": 0.2456, + "startsAt": "2024-12-06T14:00:00.000+01:00", + "level": "NORMAL" } ``` **Fields:** + - `total`: Price including VAT and fees (currency's major unit, e.g., EUR) - `startsAt`: ISO 8601 timestamp with timezone - `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) @@ -114,11 +118,12 @@ Integration stays well below these limits: ```json { - "currency": "EUR" + "currency": "EUR" } ``` Supported currencies: + - `EUR` (Euro) - displayed as ct/kWh - `NOK` (Norwegian Krone) - displayed as øre/kWh - `SEK` (Swedish Krona) - displayed as öre/kWh @@ -128,42 +133,52 @@ Supported currencies: ### Common Error Responses **Invalid Token:** + ```json { - "errors": [{ - "message": "Unauthorized", - "extensions": { - "code": "UNAUTHENTICATED" - } - }] + "errors": [ + { + "message": "Unauthorized", + "extensions": { + "code": "UNAUTHENTICATED" + } + } + ] } ``` **Rate Limit Exceeded:** + ```json { - "errors": [{ - "message": "Too Many Requests", - "extensions": { - "code": "RATE_LIMIT_EXCEEDED" - } - }] + "errors": [ + { + "message": "Too Many Requests", + "extensions": { + "code": "RATE_LIMIT_EXCEEDED" + } + } + ] } ``` **Home Not Found:** + ```json { - "errors": [{ - "message": "Home not found", - "extensions": { - "code": "NOT_FOUND" - } - }] + "errors": [ + { + "message": "Home not found", + "extensions": { + "code": "NOT_FOUND" + } + } + ] } ``` Integration handles these with: + - Exponential backoff retry (3 attempts) - ConfigEntryAuthFailed for auth errors - ConfigEntryNotReady for temporary failures @@ -171,6 +186,7 @@ Integration handles these with: ## Data Transformation Raw API data is enriched with: + - **Trailing 24h average** - Calculated from previous intervals - **Leading 24h average** - Calculated from future intervals - **Price difference %** - Deviation from average @@ -181,6 +197,7 @@ See `utils/price.py` for enrichment logic. --- 💡 **External Resources:** + - [Tibber API Documentation](https://developer.tibber.com/docs/overview) - [GraphQL Explorer](https://developer.tibber.com/explorer) - [Get API Token](https://developer.tibber.com/settings/access-token) diff --git a/docs/developer/versioned_docs/version-v0.30.0/architecture.md b/docs/developer/versioned_docs/version-v0.30.0/architecture.md index 7e0f59b..84868ec 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/architecture.md +++ b/docs/developer/versioned_docs/version-v0.30.0/architecture.md @@ -100,43 +100,43 @@ flowchart TB ### Flow Description 1. **Setup** (`__init__.py`) - - Integration loads, creates coordinator instance - - Registers entity platforms (sensor, binary_sensor) - - Sets up custom services + - Integration loads, creates coordinator instance + - Registers entity platforms (sensor, binary_sensor) + - Sets up custom services 2. **Data Fetch** (every 15 minutes) - - Coordinator triggers update via `api.py` - - API client checks **persistent cache** first (`coordinator/cache.py`) - - If cache valid → return cached data - - If cache stale → query Tibber GraphQL API - - Store fresh data in persistent cache (survives HA restart) + - Coordinator triggers update via `api.py` + - API client checks **persistent cache** first (`coordinator/cache.py`) + - If cache valid → return cached data + - If cache stale → query Tibber GraphQL API + - Store fresh data in persistent cache (survives HA restart) 3. **Price Enrichment** - - Coordinator passes raw prices to `DataTransformer` - - Transformer checks **transformation cache** (memory) - - If cache valid → return enriched data - - If cache invalid → enrich via `price_utils.py` + `average_utils.py` - - Calculate 24h trailing/leading averages - - Calculate price differences (% from average) - - Assign rating levels (LOW/NORMAL/HIGH) - - Store enriched data in transformation cache + - Coordinator passes raw prices to `DataTransformer` + - Transformer checks **transformation cache** (memory) + - If cache valid → return enriched data + - If cache invalid → enrich via `price_utils.py` + `average_utils.py` + - Calculate 24h trailing/leading averages + - Calculate price differences (% from average) + - Assign rating levels (LOW/NORMAL/HIGH) + - Store enriched data in transformation cache 4. **Period Calculation** - - Coordinator passes enriched data to `PeriodCalculator` - - Calculator computes **hash** from prices + config - - If hash matches cache → return cached periods - - If hash differs → recalculate best/peak price periods - - Store periods with new hash + - Coordinator passes enriched data to `PeriodCalculator` + - Calculator computes **hash** from prices + config + - If hash matches cache → return cached periods + - If hash differs → recalculate best/peak price periods + - Store periods with new hash 5. **Entity Updates** - - Coordinator provides complete data (prices + periods) - - Sensors read values via unified handlers - - Binary sensors evaluate period states - - Entities update on quarter-hour boundaries (00/15/30/45) + - Coordinator provides complete data (prices + periods) + - Sensors read values via unified handlers + - Binary sensors evaluate period states + - Entities update on quarter-hour boundaries (00/15/30/45) 6. **Service Calls** - - Custom services access coordinator data directly - - Return formatted responses (JSON, ApexCharts format) + - Custom services access coordinator data directly + - Return formatted responses (JSON, ApexCharts format) --- @@ -146,13 +146,13 @@ flowchart TB The integration uses **5 independent caching layers** for optimal performance: -| Layer | Location | Lifetime | Invalidation | Memory | -|-------|----------|----------|--------------|--------| -| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | -| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | -| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | -| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | -| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | +| Layer | Location | Lifetime | Invalidation | Memory | +| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ | +| **API Cache** | `coordinator/cache.py` | 24h (user)
Until midnight (prices) | Automatic | 50KB | +| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB | +| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB | +| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB | +| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB | **Total cache overhead:** ~126KB per coordinator instance (main entry + subentries) @@ -195,30 +195,31 @@ For detailed cache behavior, see [Caching Strategy](./caching-strategy.md). ### Core Components -| Component | File | Responsibility | -|-----------|------|----------------| -| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | -| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | -| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | -| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | -| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | -| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | -| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | +| Component | File | Responsibility | +| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- | +| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling | +| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance | +| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) | +| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation | +| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics | +| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) | +| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) | ### Sensor Architecture (Calculator Pattern) The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025): -| Component | Files | Lines | Responsibility | -|-----------|-------|-------|----------------| -| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | -| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | -| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | -| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | -| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | -| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | +| Component | Files | Lines | Responsibility | +| ---------------- | ------------------------- | ----- | ------------------------------------------------------- | +| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators | +| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) | +| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) | +| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping | +| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing | +| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities | **Calculator Package** (`sensor/calculators/`): + - `base.py` - Abstract BaseCalculator with coordinator access - `interval.py` - Single interval calculations (current/next/previous) - `rolling_hour.py` - 5-interval rolling windows @@ -230,6 +231,7 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns - `metadata.py` - Home/metering metadata **Benefits:** + - 58% reduction in core.py (2,170 → 909 lines) - Clear separation: Calculators (logic) vs Attributes (presentation) - Independent testability for each calculator @@ -237,12 +239,12 @@ The sensor platform uses **Calculator Pattern** for clean separation of concerns ### Helper Utilities -| Utility | File | Purpose | -|---------|------|---------| -| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | -| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | -| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | -| **Translations** | `const.py` | Translation loading and caching | +| Utility | File | Purpose | +| ----------------- | ------------------ | ------------------------------------------------- | +| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation | +| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations | +| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic | +| **Translations** | `const.py` | Translation loading and caching | --- @@ -283,12 +285,12 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: - **API polling**: Every 15 minutes (coordinator fetch cycle) - **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py` - **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)` - - HA may trigger ±few milliseconds before/after exact boundary - - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` - - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) - - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) + - HA may trigger ±few milliseconds before/after exact boundary + - Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py` + - If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data) + - If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data) - **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays) - - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) + - Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00) - **Result**: Current price sensors update without waiting for next API poll ### 4. Calculator Pattern (Sensor Platform) @@ -296,26 +298,31 @@ All quarter-hourly price intervals get augmented via `utils/price.py`: Sensors organized by **calculation method** (refactored Nov 2025): **Unified Handler Methods** (`sensor/core.py`): + - `_get_interval_value(offset, type)` - current/next/previous intervals - `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows - `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg - `_get_24h_window_value(stat_func)` - trailing/leading statistics **Routing** (`sensor/value_getters.py`): + - Single source of truth mapping 80+ entity keys to calculator methods - Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.) **Calculators** (`sensor/calculators/`): + - Each calculator inherits from `BaseCalculator` with coordinator access - Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc. - Complex logic isolated (e.g., `TrendCalculator` has internal caching) **Attributes** (`sensor/attributes/`): + - Separate from business logic, handles state presentation - Builds extra_state_attributes dicts for entity classes - Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()` **Benefits:** + - Minimal code duplication across 80+ sensors - Clear separation of concerns (calculation vs presentation) - Easy to extend: Add sensor → choose pattern → add to routing @@ -333,12 +340,12 @@ Sensors organized by **calculation method** (refactored Nov 2025): ### CPU Optimization -| Optimization | Location | Savings | -|--------------|----------|---------| -| Config caching | `coordinator/*` | ~50% on config checks | -| Period caching | `coordinator/periods.py` | ~70% on period recalculation | -| Lazy logging | Throughout | ~15% on log-heavy operations | -| Import optimization | Module structure | ~20% faster loading | +| Optimization | Location | Savings | +| ------------------- | ------------------------ | ---------------------------- | +| Config caching | `coordinator/*` | ~50% on config checks | +| Period caching | `coordinator/periods.py` | ~70% on period recalculation | +| Lazy logging | Throughout | ~15% on log-heavy operations | +| Import optimization | Module structure | ~20% faster loading | ### Memory Usage diff --git a/docs/developer/versioned_docs/version-v0.30.0/caching-strategy.md b/docs/developer/versioned_docs/version-v0.30.0/caching-strategy.md index 0c03c46..8668c97 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/caching-strategy.md +++ b/docs/developer/versioned_docs/version-v0.30.0/caching-strategy.md @@ -24,11 +24,13 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** + - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** + - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage @@ -36,29 +38,31 @@ The integration uses **4 distinct caching layers** with different purposes and l **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): - ```python - # coordinator/day_transitions.py - def _handle_midnight_turnover() -> None: - self._cached_price_data = None # Force fresh fetch for new day - self._last_price_update = None - await self.store_cache() - ``` + + ```python + # coordinator/day_transitions.py + def _handle_midnight_turnover() -> None: + self._cached_price_data = None # Force fresh fetch for new day + self._last_price_update = None + await self.store_cache() + ``` 2. **Cache validation on load**: - ```python - # coordinator/cache.py - def is_cache_valid(cache_data: CacheData) -> bool: - # Checks if price data is from a previous day - if today_date < local_now.date(): # Yesterday's data - return False - ``` + + ```python + # coordinator/cache.py + def is_cache_valid(cache_data: CacheData) -> bool: + # Checks if price data is from a previous day + if today_date < local_now.date(): # Yesterday's data + return False + ``` 3. **Tomorrow data check** (after 13:00): - ```python - # coordinator/data_fetching.py - if tomorrow_missing or tomorrow_invalid: - return "tomorrow_check" # Update needed - ``` + ```python + # coordinator/data_fetching.py + if tomorrow_missing or tomorrow_invalid: + return "tomorrow_check" # Update needed + ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. @@ -71,18 +75,22 @@ The integration uses **4 distinct caching layers** with different purposes and l **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** + - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** + - **Forever** (until HA restart) - No invalidation during runtime **When populated:** + - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** + ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") @@ -101,6 +109,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **What is cached:** ### DataTransformer Config Cache + ```python { "thresholds": {"low": 15, "high": 35}, @@ -110,6 +119,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` ### PeriodCalculator Config Cache + ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, @@ -118,20 +128,23 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Lifetime:** + - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** + - **Options change** (user reconfigures integration): - ```python - # coordinator/core.py - async def _handle_options_update(...) -> None: - self._data_transformer.invalidate_config_cache() - self._period_calculator.invalidate_config_cache() - await self.async_request_refresh() - ``` + ```python + # coordinator/core.py + async def _handle_options_update(...) -> None: + self._data_transformer.invalidate_config_cache() + self._period_calculator.invalidate_config_cache() + await self.async_request_refresh() + ``` **Performance impact:** + - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) @@ -147,6 +160,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** + ```python { "best_price": { @@ -161,6 +175,7 @@ description = get_translation("binary_sensor.best_price_period.description", "en ``` **Cache key:** Hash of relevant inputs + ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval @@ -172,6 +187,7 @@ hash_data = ( ``` **Lifetime:** + - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) @@ -179,24 +195,27 @@ hash_data = ( **Invalidation triggers:** 1. **Config change** (explicit): - ```python - def invalidate_config_cache() -> None: - self._cached_periods = None - self._last_periods_hash = None - ``` + + ```python + def invalidate_config_cache() -> None: + self._cached_periods = None + self._last_periods_hash = None + ``` 2. **Price data change** (automatic via hash mismatch): - ```python - current_hash = self._compute_periods_hash(price_info) - if self._last_periods_hash != current_hash: - # Cache miss - recalculate - ``` + ```python + current_hash = self._compute_periods_hash(price_info) + if self._last_periods_hash != current_hash: + # Cache miss - recalculate + ``` **Cache hit rate:** + - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** + - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) @@ -212,6 +231,7 @@ hash_data = ( **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** + ```python { "timestamp": ..., @@ -224,14 +244,16 @@ hash_data = ( **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** + - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - - Config changes (thresholds affect enrichment) - - Midnight turnover detected - - New update cycle begins + - Config changes (thresholds affect enrichment) + - Midnight turnover detected + - New update cycle begins **Architecture:** + - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches @@ -243,6 +265,7 @@ hash_data = ( ## Cache Invalidation Flow ### User Changes Options (Config Flow) + ``` User saves options ↓ @@ -267,6 +290,7 @@ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) + ``` Timer #2 fires at 00:00 ↓ @@ -286,6 +310,7 @@ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) + ``` Coordinator update cycle ↓ @@ -327,12 +352,14 @@ API Data Cache (price_data, user_data) ``` **No cache invalidation cascades:** + - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** + - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) @@ -341,6 +368,7 @@ API Data Cache (price_data, user_data) ## Performance Characteristics ### Typical Operation (No Changes) + ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) @@ -353,6 +381,7 @@ Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover + ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) @@ -365,6 +394,7 @@ Total: ~755ms (expected once per day) ``` ### After Config Change + ``` Options Update ├─> Cache invalidation: `<`1ms @@ -381,23 +411,25 @@ Options Update ## Summary Table -| Cache Type | Lifetime | Size | Invalidation | Purpose | -|------------|----------|------|--------------|---------| -| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | -| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | -| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | -| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | -| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | +| Cache Type | Lifetime | Size | Invalidation | Purpose | +| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- | +| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | +| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | +| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | +| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | +| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** + - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** + - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues @@ -407,7 +439,9 @@ Options Update ## Debugging Cache Issues ### Symptom: Stale data after config change + **Check:** + 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? @@ -415,7 +449,9 @@ Options Update **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating + **Check:** + 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs @@ -423,7 +459,9 @@ Options Update **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today + **Check:** + 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs @@ -431,7 +469,9 @@ Options Update **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations + **Check:** + 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys diff --git a/docs/developer/versioned_docs/version-v0.30.0/coding-guidelines.md b/docs/developer/versioned_docs/version-v0.30.0/coding-guidelines.md index c9b35e0..8e638d9 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/coding-guidelines.md +++ b/docs/developer/versioned_docs/version-v0.30.0/coding-guidelines.md @@ -8,10 +8,10 @@ comments: false ## Code Style -- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) -- **Max line length**: 120 characters -- **Max complexity**: 25 (McCabe) -- **Target**: Python 3.13 +- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort) +- **Max line length**: 120 characters +- **Max complexity**: 25 (McCabe) +- **Target**: Python 3.13 Run before committing: @@ -41,12 +41,14 @@ class TimeService: ``` **When prefix is required:** + - Public classes used across multiple modules - All exception classes - All coordinator and entity classes - Data classes (dataclasses, NamedTuples) used as public APIs **When prefix can be omitted:** + - Private helper classes within a single module (prefix with `_` underscore) - Type aliases and callbacks (e.g., `TimeServiceCallback`) - Small internal NamedTuples for function returns @@ -71,6 +73,7 @@ class DataFetcher: # Should be TibberPricesDataFetcher **Current Technical Debt:** Many existing classes lack the `TibberPrices` prefix. Before refactoring: + 1. Document the plan in `/planning/class-naming-refactoring.md` 2. Use `multi_replace_string_in_file` for bulk renames 3. Test thoroughly after each module diff --git a/docs/developer/versioned_docs/version-v0.30.0/contributing.md b/docs/developer/versioned_docs/version-v0.30.0/contributing.md index 485059d..9dee950 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/contributing.md +++ b/docs/developer/versioned_docs/version-v0.30.0/contributing.md @@ -14,10 +14,10 @@ Welcome! This guide helps you contribute to the Tibber Prices integration. 1. Fork the repository on GitHub 2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git - cd hass.tibber_prices - ``` + ```bash + git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git + cd hass.tibber_prices + ``` 3. Open in VS Code 4. Click "Reopen in Container" when prompted @@ -34,6 +34,7 @@ git checkout -b fix/issue-123-description ``` **Branch naming:** + - `feature/` - New features - `fix/` - Bug fixes - `docs/` - Documentation only @@ -45,6 +46,7 @@ git checkout -b fix/issue-123-description Edit code, following [Coding Guidelines](coding-guidelines.md). **Run checks frequently:** + ```bash ./scripts/type-check # Pyright type checking ./scripts/lint # Ruff linting (auto-fix) @@ -78,6 +80,7 @@ async def test_your_feature(hass, coordinator): ``` Run your test: + ```bash ./scripts/test tests/test_your_feature.py -v ``` @@ -97,6 +100,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." ``` **Commit types:** + - `feat:` - New feature - `fix:` - Bug fix - `docs:` - Documentation @@ -105,6 +109,7 @@ Impact: Users can predict when prices will stabilize or continue fluctuating." - `chore:` - Maintenance **Add scope when relevant:** + - `feat(sensors):` - Sensor platform - `fix(coordinator):` - Data coordinator - `docs(user):` - User documentation @@ -124,32 +129,40 @@ Then open Pull Request on GitHub. Title: Short, descriptive (50 chars max) Description should include: + ```markdown ## What + Brief description of changes ## Why + Problem being solved or feature rationale ## How + Implementation approach ## Testing + - [ ] Manual testing in Home Assistant - [ ] Unit tests added/updated - [ ] Type checking passes - [ ] Linting passes ## Breaking Changes + (If any - describe migration path) ## Related Issues + Closes #123 ``` ### PR Checklist Before submitting: + - [ ] Code follows [Coding Guidelines](coding-guidelines.md) - [ ] All tests pass (`./scripts/test`) - [ ] Type checking passes (`./scripts/type-check`) @@ -170,6 +183,7 @@ Before submitting: ### What Reviewers Look For ✅ **Good:** + - Clear, self-explanatory code - Appropriate comments for complex logic - Tests covering edge cases @@ -177,6 +191,7 @@ Before submitting: - Follows existing patterns ❌ **Avoid:** + - Large PRs (>500 lines) - split into smaller ones - Mixing unrelated changes - Missing tests for new features @@ -193,6 +208,7 @@ Before submitting: ## Finding Issues to Work On Good first issues are labeled: + - `good first issue` - Beginner-friendly - `help wanted` - Maintainers welcome contributions - `documentation` - Docs improvements @@ -210,6 +226,7 @@ Be respectful, constructive, and patient. We're all volunteers! 🙏 --- 💡 **Related:** + - [Setup Guide](setup.md) - DevContainer setup - [Coding Guidelines](coding-guidelines.md) - Style guide - [Testing](testing.md) - Writing tests diff --git a/docs/developer/versioned_docs/version-v0.30.0/critical-patterns.md b/docs/developer/versioned_docs/version-v0.30.0/critical-patterns.md index f6130b5..f8693a5 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/critical-patterns.md +++ b/docs/developer/versioned_docs/version-v0.30.0/critical-patterns.md @@ -12,6 +12,7 @@ comments: false ## 🎯 Why Are These Tests Critical? Home Assistant integrations run **continuously** in the background. Resource leaks lead to: + - **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable - **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases - **Timer Leaks**: Timers continue running after unload → unnecessary background tasks @@ -26,6 +27,7 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.1 Listener Cleanup ✅ **What is tested:** + - Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`) - Minute-update listeners are correctly removed (`async_add_minute_update_listener()`) - Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`) @@ -33,11 +35,13 @@ Home Assistant integrations run **continuously** in the background. Resource lea - Binary sensor cleanup removes ALL registered listeners **Why critical:** + - Each registered listener holds references to Entity + Coordinator - Without cleanup: Entities are not freed by GC → Memory Leak - With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed **Code Locations:** + - `coordinator/listeners.py` → `async_add_time_sensitive_listener()`, `async_add_minute_update_listener()` - `coordinator/core.py` → `register_lifecycle_callback()` - `sensor/core.py` → `async_will_remove_from_hass()` @@ -46,32 +50,38 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 1.2 Timer Cleanup ✅ **What is tested:** + - Quarter-hour timer is cancelled and reference cleared - Minute timer is cancelled and reference cleared - Both timers are cancelled together - Cleanup works even when timers are `None` **Why critical:** + - Uncancelled timers continue running after integration unload - HA's `async_track_utc_time_change()` creates persistent callbacks - Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates **Code Locations:** + - `coordinator/listeners.py` → `cancel_timers()` - `coordinator/core.py` → `async_shutdown()` #### 1.3 Config Entry Cleanup ✅ **What is tested:** + - Options update listener is registered via `async_on_unload()` - Cleanup function is correctly passed to `async_on_unload()` **Why critical:** + - `entry.add_update_listener()` registers permanent callback - Without `async_on_unload()`: Listener remains active after reload → duplicate updates - Pattern: `entry.async_on_unload(entry.add_update_listener(handler))` **Code Locations:** + - `coordinator/core.py` → `__init__()` (listener registration) - `__init__.py` → `async_unload_entry()` @@ -82,16 +92,19 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 2.1 Config Cache Invalidation **What is tested:** + - DataTransformer config cache is invalidated on options change - PeriodCalculator config + period cache is invalidated - Trend calculator cache is cleared on coordinator update **Why critical:** + - Stale config → Sensors use old user settings - Stale period cache → Incorrect best/peak price periods - Stale trend cache → Outdated trend analysis **Code Locations:** + - `coordinator/data_transformation.py` → `invalidate_config_cache()` - `coordinator/periods.py` → `invalidate_config_cache()` - `sensor/calculators/trend.py` → `clear_trend_cache()` @@ -103,15 +116,18 @@ Home Assistant integrations run **continuously** in the background. Resource lea #### 3.1 Persistent Storage Removal **What is tested:** + - Storage file is deleted on config entry removal - Cache is saved on shutdown (no data loss) **Why critical:** + - Without storage removal: Old files remain after uninstallation - Without cache save on shutdown: Data loss on HA restart - Storage path: `.storage/tibber_prices.{entry_id}` **Code Locations:** + - `__init__.py` → `async_remove_entry()` - `coordinator/core.py` → `async_shutdown()` @@ -120,12 +136,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_timer_scheduling.py` **What is tested:** + - Quarter-hour timer is registered with correct parameters - Minute timer is registered with correct parameters - Timers can be re-scheduled (override old timer) - Midnight turnover detection works correctly **Why critical:** + - Wrong timer parameters → Entities update at wrong times - Without timer override on re-schedule → Multiple parallel timers → Performance problem @@ -134,12 +152,14 @@ Home Assistant integrations run **continuously** in the background. Resource lea **File:** `tests/test_sensor_timer_assignment.py` **What is tested:** + - All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys - All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys - Both lists are disjoint (no overlap) - Sensor and binary sensor platforms are checked **Why critical:** + - Wrong timer assignment → Sensors update at wrong times - Overlap → Duplicate updates → Performance problem @@ -150,10 +170,12 @@ These patterns were analyzed and classified as **not critical**: ### 6. Async Task Management **Current Status:** Fire-and-forget pattern for short tasks + - `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds) - `coordinator/core.py` → Cache storage (short-lived, max 100ms) **Why no tests needed:** + - No long-running tasks (all < 2 seconds) - HA's event loop handles short tasks automatically - Task exceptions are already logged @@ -163,6 +185,7 @@ These patterns were analyzed and classified as **not critical**: ### 7. API Session Cleanup **Current Status:** ✅ Correctly implemented + - `async_get_clientsession(hass)` is used (shared session) - No new sessions are created - HA manages session lifecycle automatically @@ -172,6 +195,7 @@ These patterns were analyzed and classified as **not critical**: ### 8. Translation Cache Memory **Current Status:** ✅ Bounded cache + - Max ~5-10 languages × 5KB = 50KB total - Module-level cache without re-loading - Practically no memory issue @@ -181,11 +205,13 @@ These patterns were analyzed and classified as **not critical**: ### 9. Coordinator Data Structure Integrity **Current Status:** Manually tested via `./scripts/develop` + - Midnight turnover works correctly (observed over several days) - Missing keys are handled via `.get()` with defaults - 80+ sensors access `coordinator.data` without errors **Structure:** + ```python coordinator.data = { "user_data": {...}, @@ -197,6 +223,7 @@ coordinator.data = { ### 10. Service Response Memory **Current Status:** HA's response lifecycle + - HA automatically frees service responses after return - ApexCharts ~20KB response is one-time per call - No response accumulation in integration code @@ -207,29 +234,30 @@ coordinator.data = { ### ✅ Implemented Tests (41 total) -| Category | Status | Tests | File | Coverage | -|----------|--------|-------|------|----------| -| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | -| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | -| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | -| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | -| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | -| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | -| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | -| **TOTAL** | **✅** | **41** | | **100% (critical)** | +| Category | Status | Tests | File | Coverage | +| ----------------------- | ------ | ------ | --------------------------------- | ------------------- | +| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% | +| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% | +| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% | +| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% | +| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% | +| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% | +| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% | +| **TOTAL** | **✅** | **41** | | **100% (critical)** | ### 📋 Analyzed but Not Implemented (Nice-to-Have) -| Category | Status | Rationale | -|----------|--------|-----------| -| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | -| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | -| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | -| Data Structure Integrity | 📋 | Would add test time without finding real issues | -| Service Response Memory | 📋 | HA automatically frees service responses | +| Category | Status | Rationale | +| ------------------------ | ------ | ---------------------------------------------------- | +| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) | +| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) | +| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) | +| Data Structure Integrity | 📋 | Would add test time without finding real issues | +| Service Response Memory | 📋 | HA automatically frees service responses | **Legend:** + - ✅ = Fully tested or pattern verified correct - 📋 = Analyzed, low priority for testing (no known issues) @@ -238,6 +266,7 @@ coordinator.data = { ### ✅ All Critical Patterns Tested All essential memory leak prevention patterns are covered by 41 tests: + - ✅ Listeners are correctly removed (no callback leaks) - ✅ Timers are cancelled (no background task leaks) - ✅ Config entry cleanup works (no dangling listeners) diff --git a/docs/developer/versioned_docs/version-v0.30.0/debugging.md b/docs/developer/versioned_docs/version-v0.30.0/debugging.md index 7fe9c54..1f0f4ea 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/debugging.md +++ b/docs/developer/versioned_docs/version-v0.30.0/debugging.md @@ -10,9 +10,9 @@ Add to `configuration.yaml`: ```yaml logger: - default: info - logs: - custom_components.tibber_prices: debug + default: info + logs: + custom_components.tibber_prices: debug ``` Restart Home Assistant to apply. @@ -20,6 +20,7 @@ Restart Home Assistant to apply. ### Key Log Messages **Coordinator Updates:** + ``` [custom_components.tibber_prices.coordinator] Successfully fetched price data [custom_components.tibber_prices.coordinator] Cache valid, using cached data @@ -27,6 +28,7 @@ Restart Home Assistant to apply. ``` **Period Calculation:** + ``` [custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0% [custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods @@ -34,6 +36,7 @@ Restart Home Assistant to apply. ``` **API Errors:** + ``` [custom_components.tibber_prices.api] API request failed: Unauthorized [custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s @@ -47,26 +50,27 @@ Restart Home Assistant to apply. ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "Home Assistant", - "type": "debugpy", - "request": "launch", - "module": "homeassistant", - "args": ["-c", "config", "--debug"], - "justMyCode": false, - "env": { - "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" - } - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Home Assistant", + "type": "debugpy", + "request": "launch", + "module": "homeassistant", + "args": ["-c", "config", "--debug"], + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages" + } + } + ] } ``` ### Set Breakpoints **Coordinator update:** + ```python # coordinator/core.py async def _async_update_data(self) -> dict: @@ -75,6 +79,7 @@ async def _async_update_data(self) -> dict: ``` **Period calculation:** + ```python # coordinator/period_handlers/core.py def calculate_periods(...) -> list[dict]: @@ -91,6 +96,7 @@ def calculate_periods(...) -> list[dict]: ``` **Flags:** + - `-v` - Verbose output - `-s` - Show print statements - `-k pattern` - Run tests matching pattern @@ -102,6 +108,7 @@ Set breakpoint in test file, use "Debug Test" CodeLens. ### Useful Test Patterns **Print coordinator data:** + ```python def test_something(coordinator): print(f"Coordinator data: {coordinator.data}") @@ -109,6 +116,7 @@ def test_something(coordinator): ``` **Inspect period attributes:** + ```python def test_periods(hass, coordinator): periods = coordinator.data.get('best_price_periods', []) @@ -122,11 +130,13 @@ def test_periods(hass, coordinator): ### Integration Not Loading **Check:** + ```bash grep "tibber_prices" config/home-assistant.log ``` **Common causes:** + - Syntax error in Python code → Check logs for traceback - Missing dependency → Run `uv sync` - Wrong file permissions → `chmod +x scripts/*` @@ -134,12 +144,14 @@ grep "tibber_prices" config/home-assistant.log ### Sensors Not Updating **Check coordinator state:** + ```python # In Developer Tools > Template {{ states.sensor.tibber_home_current_interval_price.last_updated }} ``` **Debug in code:** + ```python # Add logging in sensor/core.py _LOGGER.debug("Updating sensor %s: old=%s new=%s", @@ -149,6 +161,7 @@ _LOGGER.debug("Updating sensor %s: old=%s new=%s", ### Period Calculation Wrong **Enable detailed period logs:** + ```python # coordinator/period_handlers/period_building.py _LOGGER.debug("Candidate intervals: %s", @@ -156,6 +169,7 @@ _LOGGER.debug("Candidate intervals: %s", ``` **Check filter statistics:** + ``` [period_building] Flex filter blocked: 45 intervals [period_building] Min distance blocked: 12 intervals @@ -200,6 +214,7 @@ python -m pstats profile.stats ### Remote Debugging with debugpy Add to coordinator code: + ```python import debugpy debugpy.listen(5678) @@ -212,11 +227,13 @@ Connect from VS Code with remote attach configuration. ### IPython REPL Install in container: + ```bash uv pip install ipython ``` Add breakpoint: + ```python from IPython import embed embed() # Drops into interactive shell @@ -225,6 +242,7 @@ embed() # Drops into interactive shell --- 💡 **Related:** + - [Testing Guide](testing.md) - Writing and running tests - [Setup Guide](setup.md) - Development environment - [Architecture](architecture.md) - Code structure diff --git a/docs/developer/versioned_docs/version-v0.30.0/intro.md b/docs/developer/versioned_docs/version-v0.30.0/intro.md index 3b58559..5c3cc35 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/intro.md +++ b/docs/developer/versioned_docs/version-v0.30.0/intro.md @@ -8,25 +8,25 @@ This is an independent, community-maintained custom integration for Home Assista ## 📚 Developer Guides -- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies -- **[Architecture](architecture.md)** - Code structure, patterns, and conventions -- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy -- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) -- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging -- **[Testing](testing.md)** - How to run tests and write new test cases -- **[Release Management](release-management.md)** - Release workflow and versioning process -- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices -- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings +- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies +- **[Architecture](architecture.md)** - Code structure, patterns, and conventions +- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy +- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers) +- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging +- **[Testing](testing.md)** - How to run tests and write new test cases +- **[Release Management](release-management.md)** - Release workflow and versioning process +- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices +- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings ## 🤖 AI Documentation The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.30.0/AGENTS.md). This file serves as long-term memory for AI assistants and contains: -- Detailed architectural patterns -- Code quality rules and conventions -- Development workflow guidance -- Common pitfalls and anti-patterns -- Project-specific patterns and utilities +- Detailed architectural patterns +- Code quality rules and conventions +- Development workflow guidance +- Common pitfalls and anti-patterns +- Project-specific patterns and utilities **Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.30.0/AGENTS.md) to keep AI guidance consistent. @@ -34,32 +34,32 @@ The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlow This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles: -- **Pattern Recognition**: Understanding and applying Home Assistant best practices -- **Code Generation**: Implementing features with proper type hints, error handling, and documentation -- **Refactoring**: Maintaining consistency across the codebase during structural changes -- **Translation Management**: Keeping 5 language files synchronized -- **Documentation**: Generating and maintaining comprehensive documentation +- **Pattern Recognition**: Understanding and applying Home Assistant best practices +- **Code Generation**: Implementing features with proper type hints, error handling, and documentation +- **Refactoring**: Maintaining consistency across the codebase during structural changes +- **Translation Management**: Keeping 5 language files synchronized +- **Documentation**: Generating and maintaining comprehensive documentation **Quality Assurance:** -- Automated linting with Ruff (120-char line length, max complexity 25) -- Home Assistant's type checking and validation -- Real-world testing in development environment -- Code review by maintainer before merging +- Automated linting with Ruff (120-char line length, max complexity 25) +- Home Assistant's type checking and validation +- Real-world testing in development environment +- Code review by maintainer before merging **Benefits:** -- Rapid feature development while maintaining quality -- Consistent code patterns across all modules -- Comprehensive documentation maintained alongside code -- Quick bug fixes with proper understanding of context +- Rapid feature development while maintaining quality +- Consistent code patterns across all modules +- Comprehensive documentation maintained alongside code +- Quick bug fixes with proper understanding of context **Limitations:** -- AI may occasionally miss edge cases or subtle bugs -- Some complex Home Assistant patterns may need human review -- Translation quality depends on AI's understanding of target language -- User feedback is crucial for discovering real-world issues +- AI may occasionally miss edge cases or subtle bugs +- Some complex Home Assistant patterns may need human review +- Translation quality depends on AI's understanding of target language +- User feedback is crucial for discovering real-world issues If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.30.0/AGENTS.md) file provides the context and patterns that ensure consistency. @@ -80,15 +80,15 @@ If you're working with AI tools on this project, the [`AGENTS.md`](https://githu The project includes several helper scripts in `./scripts/`: -- `bootstrap` - Initial setup of dependencies -- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) -- `clean` - Remove build artifacts and caches -- `lint` - Auto-fix code issues with ruff -- `lint-check` - Check code without modifications (CI mode) -- `hassfest` - Validate integration structure (JSON, Python syntax, required files) -- `setup` - Install development tools (git-cliff, @github/copilot) -- `prepare-release` - Prepare a new release (bump version, create tag) -- `generate-release-notes` - Generate release notes from commits +- `bootstrap` - Initial setup of dependencies +- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info) +- `clean` - Remove build artifacts and caches +- `lint` - Auto-fix code issues with ruff +- `lint-check` - Check code without modifications (CI mode) +- `hassfest` - Validate integration structure (JSON, Python syntax, required files) +- `setup` - Install development tools (git-cliff, @github/copilot) +- `prepare-release` - Prepare a new release (bump version, create tag) +- `generate-release-notes` - Generate release notes from commits ## 📦 Project Structure @@ -121,23 +121,23 @@ custom_components/tibber_prices/ **DataUpdateCoordinator Pattern:** -- Centralized data fetching and caching -- Automatic entity updates on data changes -- Persistent storage via `Store` -- Quarter-hour boundary refresh scheduling +- Centralized data fetching and caching +- Automatic entity updates on data changes +- Persistent storage via `Store` +- Quarter-hour boundary refresh scheduling **Price Data Enrichment:** -- Raw API data is enriched with statistical analysis -- Trailing/leading 24h averages calculated per interval -- Price differences and ratings added -- All via pure functions in `price_utils.py` +- Raw API data is enriched with statistical analysis +- Trailing/leading 24h averages calculated per interval +- Price differences and ratings added +- All via pure functions in `price_utils.py` **Translation System:** -- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) -- Both must stay in sync across all languages (de, en, nb, nl, sv) -- Async loading at integration setup +- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended) +- Both must stay in sync across all languages (de, en, nb, nl, sv) +- Async loading at integration setup ## 🧪 Testing @@ -159,18 +159,19 @@ pytest --cov=custom_components.tibber_prices tests/ Documentation is organized in two Docusaurus sites: -- **User docs** (`docs/user/`): Installation, configuration, usage guides - - Markdown files in `docs/user/docs/*.md` - - Navigation managed via `docs/user/sidebars.ts` -- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides - - Markdown files in `docs/developer/docs/*.md` - - Navigation managed via `docs/developer/sidebars.ts` -- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) +- **User docs** (`docs/user/`): Installation, configuration, usage guides + - Markdown files in `docs/user/docs/*.md` + - Navigation managed via `docs/user/sidebars.ts` +- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides + - Markdown files in `docs/developer/docs/*.md` + - Navigation managed via `docs/developer/sidebars.ts` +- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory) **Best practices:** -- Use clear examples and code snippets -- Keep docs up-to-date with code changes -- Add new pages to appropriate `sidebars.ts` for navigation + +- Use clear examples and code snippets +- Keep docs up-to-date with code changes +- Add new pages to appropriate `sidebars.ts` for navigation ## 🤝 Contributing diff --git a/docs/developer/versioned_docs/version-v0.30.0/performance.md b/docs/developer/versioned_docs/version-v0.30.0/performance.md index a9ea3d8..4208728 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/performance.md +++ b/docs/developer/versioned_docs/version-v0.30.0/performance.md @@ -5,6 +5,7 @@ Guidelines for maintaining and improving integration performance. ## Performance Goals Target metrics: + - **Coordinator update**: <500ms (typical: 200-300ms) - **Sensor update**: <10ms per sensor - **Period calculation**: <100ms (typical: 20-50ms) @@ -64,6 +65,7 @@ python -m aioprof homeassistant -c config ### Caching **1. Persistent Cache** (API data): + ```python # Already implemented in coordinator/cache.py store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @@ -71,6 +73,7 @@ data = await store.async_load() ``` **2. Translation Cache** (in-memory): + ```python # Already implemented in const.py _TRANSLATION_CACHE: dict[str, dict] = {} @@ -83,6 +86,7 @@ def get_translation(path: str, language: str) -> dict: ``` **3. Config Cache** (invalidated on options change): + ```python class DataTransformer: def __init__(self): @@ -100,6 +104,7 @@ class DataTransformer: ### Lazy Loading **Load data only when needed:** + ```python @property def extra_state_attributes(self) -> dict | None: @@ -113,6 +118,7 @@ def extra_state_attributes(self) -> dict | None: ### Bulk Operations **Process multiple items at once:** + ```python # ❌ Slow - loop with individual operations for interval in intervals: @@ -126,6 +132,7 @@ results = enrich_intervals_bulk(intervals) ### Async Best Practices **1. Concurrent API calls:** + ```python # ❌ Sequential (slow) user_data = await fetch_user_data() @@ -139,6 +146,7 @@ user_data, price_data = await asyncio.gather( ``` **2. Don't block event loop:** + ```python # ❌ Blocking result = heavy_computation() # Blocks for seconds @@ -152,6 +160,7 @@ result = await hass.async_add_executor_job(heavy_computation) ### Avoid Memory Leaks **1. Clear references:** + ```python class Coordinator: async def async_shutdown(self): @@ -162,6 +171,7 @@ class Coordinator: ``` **2. Use weak references for callbacks:** + ```python import weakref @@ -176,6 +186,7 @@ class Manager: ### Efficient Data Structures **Use appropriate types:** + ```python # ❌ List for lookups (O(n)) if timestamp in timestamp_list: @@ -197,11 +208,13 @@ results = (x for x in items if condition(x)) ### Minimize API Calls **Already implemented:** + - Cache valid until midnight - User data cached for 24h - Only poll when tomorrow data expected **Monitor API usage:** + ```python _LOGGER.debug("API call: %s (cache_age=%s)", endpoint, cache_age) @@ -210,6 +223,7 @@ _LOGGER.debug("API call: %s (cache_age=%s)", ### Smart Updates **Only update when needed:** + ```python async def _async_update_data(self) -> dict: """Fetch data from API.""" @@ -226,6 +240,7 @@ async def _async_update_data(self) -> dict: ### State Class Selection **Affects long-term statistics storage:** + ```python # ❌ MEASUREMENT for prices (stores every change) state_class=SensorStateClass.MEASUREMENT # ~35K records/year @@ -240,6 +255,7 @@ state_class=SensorStateClass.TOTAL # For cumulative values ### Attribute Size **Keep attributes minimal:** + ```python # ❌ Large nested structures (KB per update) attributes = { @@ -317,6 +333,7 @@ _LOGGER.debug("Current memory usage: %.2f MB", memory_mb) --- 💡 **Related:** + - [Caching Strategy](caching-strategy.md) - Cache layers - [Architecture](architecture.md) - System design - [Debugging](debugging.md) - Profiling tools diff --git a/docs/developer/versioned_docs/version-v0.30.0/period-calculation-theory.md b/docs/developer/versioned_docs/version-v0.30.0/period-calculation-theory.md index 01058bf..a8464a8 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/period-calculation-theory.md +++ b/docs/developer/versioned_docs/version-v0.30.0/period-calculation-theory.md @@ -7,6 +7,7 @@ This document explains the mathematical foundations and design decisions behind **Target Audience:** Developers maintaining or extending the period calculation logic. **Related Files:** + - `coordinator/period_handlers/core.py` - Main calculation entry point - `coordinator/period_handlers/level_filtering.py` - Flex and distance filtering - `coordinator/period_handlers/relaxation.py` - Multi-phase relaxation strategy @@ -23,6 +24,7 @@ Period detection uses **three independent filters** (all must pass): **Purpose:** Limit how far prices can deviate from the daily min/max. **Logic:** + ```python # Best Price: Price must be within flex% ABOVE daily minimum in_flex = price <= (daily_min + daily_min × flex) @@ -32,6 +34,7 @@ in_flex = price >= (daily_max - daily_max × flex) ``` **Example (Best Price):** + - Daily Min: 10 ct/kWh - Flex: 15% - Acceptance Range: 0 - 11.5 ct/kWh (10 + 10×0.15) @@ -41,6 +44,7 @@ in_flex = price >= (daily_max - daily_max × flex) **Purpose:** Ensure periods are **significantly** cheaper/more expensive than average, not just marginally better. **Logic:** + ```python # Best Price: Price must be at least min_distance% BELOW daily average meets_distance = price <= (daily_avg × (1 - min_distance/100)) @@ -50,6 +54,7 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) ``` **Example (Best Price):** + - Daily Avg: 15 ct/kWh - Min Distance: 5% - Acceptance Range: 0 - 14.25 ct/kWh (15 × 0.95) @@ -65,17 +70,17 @@ meets_distance = price >= (daily_avg × (1 + min_distance/100)) The integration maintains **two independent sets** of volatility thresholds: 1. **Sensor Thresholds** (user-configurable via `CONF_VOLATILITY_*_THRESHOLD`) - - Purpose: Display classification in `sensor.tibber_home_volatility_*` - - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% - - User can adjust in config flow options - - Affects: Sensor state/attributes only + - Purpose: Display classification in `sensor.tibber_home_volatility_*` + - Default: LOW < 10%, MEDIUM < 20%, HIGH ≥ 20% + - User can adjust in config flow options + - Affects: Sensor state/attributes only 2. **Period Filter Thresholds** (internal, fixed) - - Purpose: Level filter criteria when using `level="volatility_low"` etc. - - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` - - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) - - User **cannot** adjust these - - Affects: Period candidate selection + - Purpose: Level filter criteria when using `level="volatility_low"` etc. + - Source: `PRICE_LEVEL_THRESHOLDS` in `const.py` + - Values: Same as sensor defaults (LOW < 10%, MEDIUM < 20%, HIGH ≥ 20%) + - User **cannot** adjust these + - Affects: Period candidate selection **Rationale for Separation:** @@ -86,6 +91,7 @@ The integration maintains **two independent sets** of volatility thresholds: - Period calculation has many interacting filters (Flex, Distance, Level) - exposing all internals would be error-prone **Implementation:** + ```python # Sensor classification uses user config user_low_threshold = config_entry.options.get(CONF_VOLATILITY_LOW_THRESHOLD, 10) @@ -107,36 +113,42 @@ period_low_threshold = PRICE_LEVEL_THRESHOLDS["volatility_low"] # Always 10% #### Scenario: Best Price with Flex=50%, Min_Distance=5% **Given:** + - Daily Min: 10 ct/kWh - Daily Avg: 15 ct/kWh - Daily Max: 20 ct/kWh **Flex Filter (50%):** + ``` Max accepted = 10 + (10 × 0.50) = 15 ct/kWh ``` **Min Distance Filter (5%):** + ``` Max accepted = 15 × (1 - 0.05) = 14.25 ct/kWh ``` **Conflict:** + - Interval at 14.8 ct/kWh: - - ✅ Flex: 14.8 ≤ 15 (PASS) - - ❌ Distance: 14.8 > 14.25 (FAIL) - - **Result:** Rejected by Min_Distance even though Flex allows it! + - ✅ Flex: 14.8 ≤ 15 (PASS) + - ❌ Distance: 14.8 > 14.25 (FAIL) + - **Result:** Rejected by Min_Distance even though Flex allows it! **The Issue:** At high Flex values, Min_Distance becomes the dominant filter and blocks intervals that Flex would permit. This defeats the purpose of having high Flex. ### Mathematical Analysis **Conflict condition for Best Price:** + ``` daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) ``` **Typical values:** + - Min = 10, Avg = 15, Min_Distance = 5% - Conflict occurs when: `10 × (1 + flex) > 14.25` - Simplify: `flex > 0.425` (42.5%) @@ -149,6 +161,7 @@ daily_min × (1 + flex) > daily_avg × (1 - min_distance/100) **Approach:** Reduce Min_Distance proportionally as Flex increases. **Formula:** + ```python if flex > 0.20: # 20% threshold flex_excess = flex - 0.20 @@ -158,15 +171,16 @@ if flex > 0.20: # 20% threshold **Scaling Table (Original Min_Distance = 5%):** -| Flex | Scale Factor | Adjusted Min_Distance | Rationale | -|-------|--------------|----------------------|-----------| -| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | -| 25% | 0.88 | 4.4% | Slight reduction | -| 30% | 0.75 | 3.75% | Moderate reduction | -| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | -| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | +| Flex | Scale Factor | Adjusted Min_Distance | Rationale | +| ---- | ------------ | --------------------- | --------------------------------- | +| ≤20% | 1.00 | 5.0% | Standard - both filters relevant | +| 25% | 0.88 | 4.4% | Slight reduction | +| 30% | 0.75 | 3.75% | Moderate reduction | +| 40% | 0.50 | 2.5% | Strong reduction - Flex dominates | +| 50% | 0.25 | 1.25% | Minimal distance - Flex decides | **Why stop at 25% of original?** + - Min_Distance ensures periods are **significantly** different from average - Even at 1.25%, prevents "flat days" (little price variation) from accepting every interval - Maintains semantic meaning: "this is a meaningful best/peak price period" @@ -174,6 +188,7 @@ if flex > 0.20: # 20% threshold **Implementation:** See `level_filtering.py` → `check_interval_criteria()` **Code Extract:** + ```python # coordinator/period_handlers/level_filtering.py @@ -209,12 +224,14 @@ def check_interval_criteria(price, criteria): ``` **Why Linear Scaling?** + - Simple and predictable - No abrupt behavior changes - Easy to reason about for users and developers - Alternative considered: Exponential scaling (rejected as too aggressive) **Why 25% Minimum?** + - Below this, min_distance loses semantic meaning - Even on flat days, some quality filter needed - Prevents "every interval is a period" scenario @@ -227,12 +244,14 @@ def check_interval_criteria(price, criteria): ### Implementation Constants **Defined in `coordinator/period_handlers/core.py`:** + ```python MAX_SAFE_FLEX = 0.50 # 50% - hard cap: above this, period detection becomes unreliable MAX_OUTLIER_FLEX = 0.25 # 25% - cap for outlier filtering: above this, spike detection too permissive ``` **Defined in `const.py`:** + ```python DEFAULT_BEST_PRICE_FLEX = 15 # 15% base - optimal for relaxation mode (default enabled) DEFAULT_PEAK_PRICE_FLEX = -20 # 20% base (negative for peak detection) @@ -255,16 +274,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Find practical time windows for running appliances **Constraints:** + - Appliances need time to complete cycles (dishwasher: 2-3h, EV charging: 4-8h) - Short periods are impractical (not worth automation overhead) - User wants genuinely cheap times, not just "slightly below average" **Defaults:** + - **60 min minimum** - Ensures period is long enough for meaningful use - **15% flex** - Stricter selection, focuses on truly cheap times - **Reasoning:** Better to find fewer, higher-quality periods than many mediocre ones **User behavior:** + - Automations trigger actions (turn on devices) - Wrong automation = wasted energy/money - Preference: Conservative (miss some savings) over aggressive (false positives) @@ -274,16 +296,19 @@ The different defaults reflect fundamentally different use cases: **Goal:** Alert users to expensive periods for consumption reduction **Constraints:** + - Brief price spikes still matter (even 15-30 min is worth avoiding) - Early warning more valuable than perfect accuracy - User can manually decide whether to react **Defaults:** + - **30 min minimum** - Catches shorter expensive spikes - **20% flex** - More permissive, earlier detection - **Reasoning:** Better to warn early (even if not peak) than miss expensive periods **User behavior:** + - Notifications/alerts (informational) - Wrong alert = minor inconvenience, not cost - Preference: Sensitive (catch more) over specific (catch only extremes) @@ -293,17 +318,20 @@ The different defaults reflect fundamentally different use cases: **Peak Price Volatility:** Price curves tend to have: + - **Sharp spikes** during peak hours (morning/evening) - **Shorter duration** at maximum (1-2 hours typical) - **Higher variance** in peak times than cheap times **Example day:** + ``` Cheap period: 02:00-07:00 (5 hours at 10-12 ct) ← Gradual, stable Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief ``` **Implication:** + - Stricter flex on peak (15%) might miss real expensive periods (too brief) - Longer min_length (60 min) might exclude legitimate spikes - Solution: More flexible thresholds for peak detection @@ -311,16 +339,19 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief #### Design Alternatives Considered **Option 1: Symmetric defaults (rejected)** + - Both 60 min, both 15% flex - Problem: Misses short but expensive spikes - User feedback: "Why didn't I get warned about the 30-min price spike?" **Option 2: Same defaults, let users figure it out (rejected)** + - No guidance on best practices - Users would need to experiment to find good values - Most users stick with defaults, so defaults matter **Option 3: Current approach (adopted)** + - **All values user-configurable** via config flow options - **Different installation defaults** for Best Price vs. Peak Price - Defaults reflect recommended practices for each use case @@ -336,12 +367,14 @@ Expensive period: 17:00-18:30 (1.5 hours at 35-40 ct) ← Sharp, brief **Enforcement:** `core.py` caps `abs(flex)` at 0.50 (50%) **Rationale:** + - Above 50%, period detection becomes unreliable - Best Price: Almost entire day qualifies (Min + 50% typically covers 60-80% of intervals) - Peak Price: Similar issue with Max - 50% - **Result:** Either massive periods (entire day) or no periods (min_length not met) **Warning Message:** + ``` Flex XX% exceeds maximum safe value! Capping at 50%. Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation. @@ -352,6 +385,7 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation **Enforcement:** `core.py` caps outlier filtering flex at 0.25 (25%) **Rationale:** + - Outlier filtering uses Flex to determine "stable context" threshold - At > 25% Flex, almost any price swing is considered "stable" - **Result:** Legitimate price shifts aren't smoothed, breaking period formation @@ -363,23 +397,28 @@ Recommendation: Use 15-20% with relaxation enabled, or 25-35% without relaxation #### With Relaxation Enabled (Recommended) **Optimal:** 10-20% + - Relaxation increases Flex incrementally: 15% → 18% → 21% → ... - Low baseline ensures relaxation has room to work **Warning Threshold:** > 25% + - INFO log: "Base flex is on the high side" **High Warning:** > 30% + - WARNING log: "Base flex is very high for relaxation mode!" - Recommendation: Lower to 15-20% #### Without Relaxation **Optimal:** 20-35% + - No automatic adjustment, must be sufficient from start - Higher baseline acceptable since no relaxation fallback **Maximum Useful:** ~50% + - Above this, period detection degrades (see Hard Limits) --- @@ -397,6 +436,7 @@ These three mechanisms handle pathological price situations where standard filte **Problem:** When all prices are nearly identical (e.g. 28–32 ct, CV=5.4%), requiring 2 distinct "best price" windows is geometrically impossible. Even after exhausting all 11 relaxation phases, only 1 period exists because there is no second cheap cluster. **Solution:** Before the baseline counting loop, compute per-day effective min_periods: + ```python if day_cv <= 10%: day_effective_min[day] = 1 # Flat day: 1 period is enough @@ -417,17 +457,18 @@ else: **Problem:** On solar surplus days (avg 2–5 ct/kWh), a percentage-based min_distance like 5% means only 0.1 ct absolute separation is required. The filter either accepts almost the entire day (if ref_price is 2 ct, 5% = 0.1 ct – nearly everything qualifies) or blocks everything (if the spread is within that 0.1 ct band). **Solution:** Linear scaling toward zero as avg_price approaches zero: + ``` scale_factor = avg_price / LOW_PRICE_AVG_THRESHOLD adjusted_min_distance = original_min_distance × scale_factor ``` -| avg_price | scale | Effect on 5% min_distance | -|---|---|---| -| ≥ 10 ct (0.10 EUR) | 100% | 5% (full distance) | -| 5 ct (0.05 EUR) | 50% | 2.5% | -| 2 ct (0.02 EUR) | 20% | 1% | -| 0 ct | 0% | 0% (disabled) | +| avg_price | scale | Effect on 5% min_distance | +| ------------------ | ----- | ------------------------- | +| ≥ 10 ct (0.10 EUR) | 100% | 5% (full distance) | +| 5 ct (0.05 EUR) | 50% | 2.5% | +| 2 ct (0.02 EUR) | 20% | 1% | +| 0 ct | 0% | 0% (disabled) | ### 3. CV Quality Gate Bypass for Absolute Low-Price Periods @@ -435,11 +476,12 @@ adjusted_min_distance = original_min_distance × scale_factor **Trigger:** Period mean price < `LOW_PRICE_QUALITY_BYPASS_THRESHOLD` (0.10 EUR) -**Problem:** A period at 0.5–4 ct has high *relative* variation (CV ≈ 70–80%), but the absolute differences are fractions of a cent. The quality gate (CV ≤ `PERIOD_MAX_CV`) with a relative metric would wrongly reject this as a "heterogeneous" period. +**Problem:** A period at 0.5–4 ct has high _relative_ variation (CV ≈ 70–80%), but the absolute differences are fractions of a cent. The quality gate (CV ≤ `PERIOD_MAX_CV`) with a relative metric would wrongly reject this as a "heterogeneous" period. **Distinguishes from flat normal days:** A flat day at 33–36 ct also has low absolute range, but mean is 34.5 ct (>> 0.10 EUR threshold). The bypass only applies when the mean itself is below the threshold – i.e. the day is genuinely cheap in absolute terms. **Solution:** Short-circuit the quality gate check: + ```python period_mean = sum(period_prices) / len(period_prices) if period_mean < LOW_PRICE_QUALITY_BYPASS_THRESHOLD: @@ -470,6 +512,7 @@ Ensure **minimum periods per day** are found even when baseline filters are too ### Multi-Phase Approach **Each day processed independently:** + 1. Calculate baseline periods with user's config 2. If insufficient periods found, enter relaxation loop 3. Try progressively relaxed filter combinations @@ -493,6 +536,7 @@ for attempt in range(max_relaxation_attempts): ``` **Constants:** + ```python FLEX_WARNING_THRESHOLD_RELAXATION = 0.25 # 25% - INFO: suggest lowering to 15-20% FLEX_HIGH_THRESHOLD_RELAXATION = 0.30 # 30% - WARNING: very high for relaxation mode @@ -502,26 +546,27 @@ MAX_FLEX_HARD_LIMIT = 0.50 # 50% - absolute maximum (enforced in core.py) **Design Decisions:** 1. **Why 3% fixed increment?** - - Predictable escalation path (15% → 18% → 21% → ...) - - Independent of base flex (works consistently) - - 11 attempts covers full useful range (15% → 48%) - - Balance: Not too slow (2%), not too fast (5%) + - Predictable escalation path (15% → 18% → 21% → ...) + - Independent of base flex (works consistently) + - 11 attempts covers full useful range (15% → 48%) + - Balance: Not too slow (2%), not too fast (5%) 2. **Why hard-coded, not configurable?** - - Prevents user misconfiguration - - Simplifies mental model (fewer knobs to turn) - - Reliable behavior across all configurations - - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) + - Prevents user misconfiguration + - Simplifies mental model (fewer knobs to turn) + - Reliable behavior across all configurations + - If needed, user adjusts `max_relaxation_attempts` (fewer/more steps) 3. **Why warn at 25% base flex?** - - At 25% base, first relaxation step reaches 28% - - Above 30%, entering diminishing returns territory - - User likely doesn't need relaxation with such high base flex - - Should either: (a) lower base flex, or (b) disable relaxation + - At 25% base, first relaxation step reaches 28% + - Above 30%, entering diminishing returns territory + - User likely doesn't need relaxation with such high base flex + - Should either: (a) lower base flex, or (b) disable relaxation **Historical Context (Pre-November 2025):** The algorithm previously used percentage-based increments that scaled with base flex: + ```python increment = base_flex × (step_pct / 100) # REMOVED ``` @@ -529,6 +574,7 @@ increment = base_flex × (step_pct / 100) # REMOVED This caused exponential escalation with high base flex values (e.g., 40% → 50% → 60% → 70% in just 6 steps), making behavior unpredictable. The fixed 3% increment solves this by providing consistent, controlled escalation regardless of starting point. **Warning Messages:** + ```python if base_flex >= FLEX_HIGH_THRESHOLD_RELAXATION: # 30% _LOGGER.warning( @@ -547,12 +593,14 @@ elif base_flex >= FLEX_WARNING_THRESHOLD_RELAXATION: # 25% ### Filter Combination Strategy **Per Flex level, try in order:** + 1. Original Level filter 2. Level filter = "any" (disabled) **Early Exit:** Stop immediately when target reached (don't try unnecessary combinations) **Example Flow (target=2 periods/day):** + ``` Day 2025-11-19: 1. Baseline flex=15%: Found 1 period (need 2) @@ -567,6 +615,7 @@ Day 2025-11-19: ### Key Files and Functions **Period Calculation Entry Point:** + ```python # coordinator/period_handlers/core.py def calculate_periods( @@ -577,6 +626,7 @@ def calculate_periods( ``` **Flex + Distance Filtering:** + ```python # coordinator/period_handlers/level_filtering.py def check_interval_criteria( @@ -586,6 +636,7 @@ def check_interval_criteria( ``` **Relaxation Orchestration:** + ```python # coordinator/period_handlers/relaxation.py def calculate_periods_with_relaxation(...) -> tuple[dict, dict] @@ -601,43 +652,45 @@ def relax_single_day(...) -> tuple[dict, dict] **Algorithm Details:** 1. **Linear Regression Prediction:** - - Uses surrounding intervals to predict expected price - - Window size: 3+ intervals (MIN_CONTEXT_SIZE) - - Calculates trend slope and standard deviation - - Formula: `predicted = mean + slope × (position - center)` + - Uses surrounding intervals to predict expected price + - Window size: 3+ intervals (MIN_CONTEXT_SIZE) + - Calculates trend slope and standard deviation + - Formula: `predicted = mean + slope × (position - center)` 2. **Confidence Intervals:** - - 95% confidence level (2 standard deviations) - - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) - - Outlier if: `|actual - predicted| > tolerance` - - Accounts for natural price volatility in context window + - 95% confidence level (2 standard deviations) + - Tolerance = 2.0 × std_dev (CONFIDENCE_LEVEL constant) + - Outlier if: `|actual - predicted| > tolerance` + - Accounts for natural price volatility in context window 3. **Symmetry Check:** - - Rejects asymmetric outliers (threshold: 1.5 std dev) - - Preserves legitimate price shifts (morning/evening peaks) - - Algorithm: - ```python - residual = abs(actual - predicted) - symmetry_threshold = 1.5 × std_dev + - Rejects asymmetric outliers (threshold: 1.5 std dev) + - Preserves legitimate price shifts (morning/evening peaks) + - Algorithm: - if residual > tolerance: - # Check if spike is symmetric in context - context_residuals = [abs(p - pred) for p, pred in context] - avg_context_residual = mean(context_residuals) + ```python + residual = abs(actual - predicted) + symmetry_threshold = 1.5 × std_dev - if residual > symmetry_threshold × avg_context_residual: - # Asymmetric spike → smooth it - else: - # Symmetric (part of trend) → keep it - ``` + if residual > tolerance: + # Check if spike is symmetric in context + context_residuals = [abs(p - pred) for p, pred in context] + avg_context_residual = mean(context_residuals) + + if residual > symmetry_threshold × avg_context_residual: + # Asymmetric spike → smooth it + else: + # Symmetric (part of trend) → keep it + ``` 4. **Enhanced Zigzag Detection:** - - Detects spike clusters via relative volatility - - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) - - Single-pass algorithm (no iteration needed) - - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) + - Detects spike clusters via relative volatility + - Threshold: 2.0× local volatility (RELATIVE_VOLATILITY_THRESHOLD) + - Single-pass algorithm (no iteration needed) + - Catches patterns like: 18, 35, 19, 34, 18 (alternating spikes) **Constants:** + ```python # coordinator/period_handlers/outlier_filtering.py @@ -648,18 +701,21 @@ MIN_CONTEXT_SIZE = 3 # Minimum intervals for regression ``` **Data Integrity:** + - Original prices stored in `_original_price` field - All statistics (daily min/max/avg) use original prices - Smoothing only affects period formation logic - Smart counting: Only counts smoothing that changed period outcome **Performance:** + - Single pass through price data - O(n) complexity with small context window - No iterative refinement needed - Typical processing time: `<`1ms for 96 intervals **Example Debug Output:** + ``` DEBUG: [2025-11-11T14:30:00+01:00] Outlier detected: 35.2 ct DEBUG: Context: 18.5, 19.1, 19.3, 19.8, 20.2 ct @@ -673,19 +729,19 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier **Why This Approach?** 1. **Linear regression over moving average:** - - Accounts for price trends (morning ramp-up, evening decline) - - Moving average can't predict direction, only level - - Better accuracy on non-stationary price curves + - Accounts for price trends (morning ramp-up, evening decline) + - Moving average can't predict direction, only level + - Better accuracy on non-stationary price curves 2. **Symmetry check over fixed threshold:** - - Prevents false positives on legitimate price shifts - - Adapts to local volatility patterns - - Preserves user expectation: "expensive during peak hours" + - Prevents false positives on legitimate price shifts + - Adapts to local volatility patterns + - Preserves user expectation: "expensive during peak hours" 3. **Single-pass over iterative:** - - Predictable behavior (no convergence issues) - - Fast and deterministic - - Easier to debug and reason about + - Predictable behavior (no convergence issues) + - Fast and deterministic + - Easier to debug and reason about **Alternative Approaches Considered:** @@ -699,15 +755,17 @@ DEBUG: Asymmetry ratio: 3.2 (>1.5 threshold) → confirmed outlier ## Debugging Tips **Enable DEBUG logging:** + ```yaml # configuration.yaml logger: - default: info - logs: - custom_components.tibber_prices.coordinator.period_handlers: debug + default: info + logs: + custom_components.tibber_prices.coordinator.period_handlers: debug ``` **Key log messages to watch:** + 1. `"Filter statistics: X intervals checked"` - Shows how many intervals filtered by each criterion 2. `"After build_periods: X raw periods found"` - Periods before min_length filtering 3. `"Day X: Success with flex=Y%"` - Relaxation succeeded @@ -720,52 +778,61 @@ logger: ### ❌ Anti-Pattern 1: High Flex with Relaxation **Configuration:** + ```yaml best_price_flex: 40 enable_relaxation_best: true ``` **Problem:** + - Base Flex 40% already very permissive - Relaxation increments further (43%, 46%, 49%, ...) - Quickly approaches 50% cap with diminishing returns **Solution:** + ```yaml -best_price_flex: 15 # Let relaxation increase it +best_price_flex: 15 # Let relaxation increase it enable_relaxation_best: true ``` ### ❌ Anti-Pattern 2: Zero Min_Distance **Configuration:** + ```yaml best_price_min_distance_from_avg: 0 ``` **Problem:** + - "Flat days" (little price variation) accept all intervals - Periods lose semantic meaning ("significantly cheap") - May create periods during barely-below-average times **Solution:** + ```yaml -best_price_min_distance_from_avg: 5 # Use default 5% +best_price_min_distance_from_avg: 5 # Use default 5% ``` ### ❌ Anti-Pattern 3: Conflicting Flex + Distance **Configuration:** + ```yaml best_price_flex: 45 best_price_min_distance_from_avg: 10 ``` **Problem:** + - Distance filter dominates, making Flex irrelevant - Dynamic scaling helps but still suboptimal **Solution:** + ```yaml best_price_flex: 20 best_price_min_distance_from_avg: 5 @@ -781,11 +848,13 @@ best_price_min_distance_from_avg: 5 **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: Should find 2-4 clear best price periods - Flex 30%: Should find 4-8 periods (more lenient) - Min_Distance 5%: Effective throughout range **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 12/96 (12.5%) ← Low percentage = good variation @@ -799,11 +868,13 @@ DEBUG: After build_periods: 3 raw periods found **Average:** 15 ct/kWh **Expected Behavior:** + - Flex 15%: May find 1-2 small periods (or zero if no clear winners) - Min_Distance 5%: Critical here - ensures only truly cheaper intervals qualify - Without Min_Distance: Would accept almost entire day as "best price" **Debug Checks:** + ``` DEBUG: Filter statistics: 96 intervals checked DEBUG: Filtered by FLEX: 45/96 (46.9%) ← High percentage = poor variation @@ -825,6 +896,7 @@ Relaxation would exhaust all 11 phases trying to find a second period. All price `_compute_day_effective_min()` detects CV ≤ 10% and sets `day_effective_min = 1` for this day. The result is accepted after finding the single cheapest cluster. **Expected Logs:** + ``` DEBUG: Day 2025-11-11: flat price profile (CV=5.4% ≤ 10.0%) → min_periods relaxed to 1 INFO: Adaptive min_periods: 1 flat day(s) (CV ≤ 10%) need only 1 period instead of 2 @@ -832,10 +904,11 @@ INFO: Day 2025-11-11: Baseline satisfied (1 period, effective minimum is 1) ``` **Sensor Attributes:** + ```yaml -min_periods_configured: 2 # User's setting -periods_found_total: 1 # Actual result -flat_days_detected: 1 # Explains the difference +min_periods_configured: 2 # User's setting +periods_found_total: 1 # Actual result +flat_days_detected: 1 # Explains the difference ``` **Why not for Peak Price?** @@ -848,18 +921,20 @@ Peak price always runs full relaxation. On a flat day, the integration still nee **Configuration:** `min_periods_best: 2`, 5% min_distance **Problems without fixes:** -1. **min_distance conflict:** 5% of 2.1 ct = 0.105 ct minimum distance. Only prices ≤ 1.995 ct qualify. The daily minimum is 0.5 ct – well within range. But the *relative* threshold becomes meaninglessly tiny: the entire day could qualify. + +1. **min_distance conflict:** 5% of 2.1 ct = 0.105 ct minimum distance. Only prices ≤ 1.995 ct qualify. The daily minimum is 0.5 ct – well within range. But the _relative_ threshold becomes meaninglessly tiny: the entire day could qualify. 2. **CV quality gate:** Prices 0.5–4.2 ct show high relative variation (CV ≈ 70-80%), but the absolute differences are fractions of a cent. The quality gate would wrongly reject valid periods. **Implemented behavior:** -*`LOW_PRICE_AVG_THRESHOLD = 0.10 EUR` (level_filtering.py):* +_`LOW_PRICE_AVG_THRESHOLD = 0.10 EUR` (level_filtering.py):_ When `avg_price < 0.10 EUR`, min_distance is scaled linearly to 0. At avg=2.1 ct (0.021 EUR), scale ≈ 21% → min_distance effectively 1%. Prevents the distance filter from blocking the entire day or accepting the entire day. -*`LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 EUR` (relaxation.py):* +_`LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 EUR` (relaxation.py):_ When period mean < 0.10 EUR, the CV quality gate is bypassed entirely. A period at 0.5–2 ct with CV=60% is practically homogeneous from a cost perspective. **Expected Logs:** + ``` DEBUG: Low-price day (avg=0.021 EUR < 0.10 threshold): min_distance scaled 5% → 1.1% DEBUG: Period 02:00-05:00: mean=0.009 EUR < bypass threshold → quality gate bypassed @@ -871,11 +946,13 @@ DEBUG: Period 02:00-05:00: mean=0.009 EUR < bypass threshold → quality gate **Average:** 18 ct/kWh **Expected Behavior:** + - Flex 15%: Finds multiple very cheap periods (5-6 ct) - Outlier filtering: May smooth isolated spikes (30-40 ct) - Distance filter: Less impactful (clear separation between cheap/expensive) **Debug Checks:** + ``` DEBUG: Outlier detected: 38.5 ct (threshold: 4.2 ct) DEBUG: Smoothed to: 20.1 ct (trend prediction) @@ -890,6 +967,7 @@ DEBUG: After build_periods: 4 raw periods found **Initial State:** Baseline finds 1 period, target is 2 **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 1 period (need 2) @@ -905,6 +983,7 @@ INFO: Day 2025-11-11: Success after 1 relaxation phase (2 periods) **Initial State:** Strict filters, very flat day **Expected Flow:** + ``` INFO: Calculating BEST PRICE periods: relaxation=ON, target=2/day, flex=15.0% DEBUG: Day 2025-11-11: Baseline found 0 periods (need 2) @@ -918,50 +997,50 @@ INFO: Period calculation completed: 1/2 days reached target When debugging period calculation issues: 1. **Check Filter Statistics** - - Which filter blocks most intervals? (flex, distance, or level) - - High flex filtering (>30%) = Need more flexibility or relaxation - - High distance filtering (>50%) = Min_distance too strict or flat day - - High level filtering = Level filter too restrictive + - Which filter blocks most intervals? (flex, distance, or level) + - High flex filtering (>30%) = Need more flexibility or relaxation + - High distance filtering (>50%) = Min_distance too strict or flat day + - High level filtering = Level filter too restrictive 2. **Check Relaxation Behavior** - - Did relaxation activate? Check for "Baseline insufficient" message - - Which phase succeeded? Early success (phase 1-3) = good config - - Late success (phase 8-11) = Consider adjusting base config - - Exhausted all phases = Unrealistic target for this day's price curve + - Did relaxation activate? Check for "Baseline insufficient" message + - Which phase succeeded? Early success (phase 1-3) = good config + - Late success (phase 8-11) = Consider adjusting base config + - Exhausted all phases = Unrealistic target for this day's price curve 3. **Check Flex Warnings** - - INFO at 25% base flex = On the high side - - WARNING at 30% base flex = Too high for relaxation - - If seeing these: Lower base flex to 15-20% + - INFO at 25% base flex = On the high side + - WARNING at 30% base flex = Too high for relaxation + - If seeing these: Lower base flex to 15-20% 4. **Check Min_Distance Scaling** - - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" - - If scale factor `<`0.8 (20% reduction): High flex is active - - If periods still not found: Filters conflict even with scaling + - Debug messages show "High flex X% detected: Reducing min_distance Y% → Z%" + - If scale factor `<`0.8 (20% reduction): High flex is active + - If periods still not found: Filters conflict even with scaling 5. **Check Outlier Filtering** - - Look for "Outlier detected" messages - - Check `period_interval_smoothed_count` attribute - - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels + - Look for "Outlier detected" messages + - Check `period_interval_smoothed_count` attribute + - If no smoothing but periods fragmented: Not isolated spikes, but legitimate price levels 6. **Check Flat Day / Low-Price Adaptations** - - Sensor shows `flat_days_detected: N` → CV ≤ 10%, adaptive min_periods reduced target to 1 - - Sensor shows `relaxation_incomplete: true` without `flat_days_detected` → check filter settings - - Low absolute prices (avg < 10 ct): min_distance is auto-scaled, CV quality gate is bypassed - - To confirm: Enable DEBUG logging for `custom_components.tibber_prices.coordinator.period_handlers.relaxation.details` - - Look for: `"flat price profile (CV=X.X% ≤ 10.0%) → min_periods relaxed to 1"` - - Look for: `"Low-price day (avg=0.0XX EUR < 0.10 threshold): min_distance scaled X% → Y%"` + - Sensor shows `flat_days_detected: N` → CV ≤ 10%, adaptive min_periods reduced target to 1 + - Sensor shows `relaxation_incomplete: true` without `flat_days_detected` → check filter settings + - Low absolute prices (avg < 10 ct): min_distance is auto-scaled, CV quality gate is bypassed + - To confirm: Enable DEBUG logging for `custom_components.tibber_prices.coordinator.period_handlers.relaxation.details` + - Look for: `"flat price profile (CV=X.X% ≤ 10.0%) → min_periods relaxed to 1"` + - Look for: `"Low-price day (avg=0.0XX EUR < 0.10 threshold): min_distance scaled X% → Y%"` **Diagnostic Sensor Attributes Summary:** -| Attribute | Type | When shown | Meaning | -|---|---|---|---| -| `min_periods_configured` | int | Always | User's configured target per day | -| `periods_found_total` | int | Always | Actual periods found across all days | -| `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | -| `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | -| `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | -| `relaxation_level` | string | Only when active | Flex% and filter combo that succeeded | +| Attribute | Type | When shown | Meaning | +| ------------------------ | ------ | ---------------- | ------------------------------------------- | +| `min_periods_configured` | int | Always | User's configured target per day | +| `periods_found_total` | int | Always | Actual periods found across all days | +| `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | +| `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | +| `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | +| `relaxation_level` | string | Only when active | Flex% and filter combo that succeeded | --- @@ -970,19 +1049,19 @@ When debugging period calculation issues: ### Potential Improvements 1. **Adaptive Flex Calculation:** - - Auto-adjust Flex based on daily price variation - - High variation days: Lower Flex needed - - Low variation days: Higher Flex needed + - Auto-adjust Flex based on daily price variation + - High variation days: Lower Flex needed + - Low variation days: Higher Flex needed 2. **Machine Learning Approach:** - - Learn optimal Flex/Distance from user feedback - - Classify days by pattern (normal/flat/volatile/bimodal) - - Apply pattern-specific defaults + - Learn optimal Flex/Distance from user feedback + - Classify days by pattern (normal/flat/volatile/bimodal) + - Apply pattern-specific defaults 3. **Multi-Objective Optimization:** - - Balance period count vs. quality - - Consider period duration vs. price level - - Optimize for user's stated use case (EV charging vs. heat pump) + - Balance period count vs. quality + - Consider period duration vs. price level + - Optimize for user's stated use case (EV charging vs. heat pump) ### Known Limitations @@ -1001,6 +1080,7 @@ When debugging period calculation issues: **Concept:** Auto-adjust Flex based on daily price variation **Algorithm:** + ```python # Pseudo-code for adaptive flex variation = (daily_max - daily_min) / daily_avg @@ -1014,11 +1094,13 @@ else: # Normal day ``` **Benefits:** + - Eliminates need for relaxation on most days - Self-adjusting to market conditions - Better user experience (less configuration needed) **Challenges:** + - Harder to predict behavior (less transparent) - May conflict with user's mental model - Needs extensive testing across different markets @@ -1030,17 +1112,20 @@ else: # Normal day **Concept:** Learn optimal Flex/Distance from user feedback **Approach:** + - Track which periods user actually uses (automation triggers) - Classify days by pattern (normal/flat/volatile/bimodal) - Apply pattern-specific defaults - Learn per-user preferences over time **Benefits:** + - Personalized to user's actual behavior - Adapts to local market patterns - Could discover non-obvious patterns **Challenges:** + - Requires user feedback mechanism (not implemented) - Privacy concerns (storing usage patterns) - Complexity for users to understand "why this period?" @@ -1053,22 +1138,26 @@ else: # Normal day **Concept:** Balance multiple goals simultaneously **Goals:** + - Period count vs. quality (cheap vs. very cheap) - Period duration vs. price level (long mediocre vs. short excellent) - Temporal distribution (spread throughout day vs. clustered) - User's stated use case (EV charging vs. heat pump vs. dishwasher) **Algorithm:** + - Pareto optimization (find trade-off frontier) - User chooses point on frontier via preferences - Genetic algorithm or simulated annealing **Benefits:** + - More sophisticated period selection - Better match to user's actual needs - Could handle complex appliance requirements **Challenges:** + - Much more complex to implement - Harder to explain to users - Computational cost (may need caching) @@ -1083,14 +1172,17 @@ else: # Normal day **Current:** 3% cap may be too aggressive for very low base Flex **Example:** + - Base flex 5% + 3% increment = 8% (60% increase!) - Base flex 15% + 3% increment = 18% (20% increase) **Possible Solution:** + - Percentage-based increment: `increment = max(base_flex × 0.20, 0.03)` - This gives: 5% → 6% (20%), 15% → 18% (20%), 40% → 43% (7.5%) **Why Not Implemented:** + - Very low base flex (`<`10%) unusual - Users with strict requirements likely disable relaxation - Simplicity preferred over edge case optimization @@ -1100,6 +1192,7 @@ else: # Normal day **Current:** Linear scaling may be too aggressive/conservative **Alternative:** Non-linear curve + ```python # Example: Exponential scaling scale_factor = 0.25 + 0.75 × exp(-5 × (flex - 0.20)) @@ -1109,6 +1202,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) ``` **Why Not Implemented:** + - Linear is easier to reason about - No evidence that non-linear is better - Would need extensive testing @@ -1118,15 +1212,18 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Issue:** May find all periods in one part of day **Example:** + - All 3 "best price" periods between 02:00-08:00 - No periods in evening (when user might want to run appliances) **Possible Solution:** + - Add "spread" parameter (prefer distributed periods) - Weight periods by time-of-day preferences - Consider user's typical usage patterns **Why Not Implemented:** + - Adds complexity - Users can work around with multiple automations - Different users have different needs (no one-size-fits-all) @@ -1138,6 +1235,7 @@ scale_factor = 0.25 + 0.75 / (1 + exp(10 × (flex - 0.35))) **Design Principle:** Each interval is evaluated using its **own day's** reference prices (daily min/max/avg). **Implementation:** + ```python # In period_building.py build_periods(): for price_data in all_prices: @@ -1189,6 +1287,7 @@ Period crossing midnight: 23:45 Day 1 → 00:15 Day 2 **Trade-off: Periods May Break at Midnight** When days differ significantly, period can split: + ``` Day 1: Min=10ct, Avg=20ct, 23:45=11ct → ✅ Cheap (relative to Day 1) Day 2: Min=25ct, Avg=35ct, 00:00=21ct → ❌ Expensive (relative to Day 2) @@ -1200,6 +1299,7 @@ This is **mathematically correct** - 21ct is genuinely expensive on a day where **Market Reality Explains Price Jumps:** Day-ahead electricity markets (EPEX SPOT) set prices at 12:00 CET for all next-day hours: + - Late intervals (23:45): Priced ~36h before delivery → high forecast uncertainty → risk premium - Early intervals (00:00): Priced ~12h before delivery → better forecasts → lower risk buffer @@ -1208,15 +1308,17 @@ This explains why absolute prices jump at midnight despite minimal demand change **User-Facing Solution (Nov 2025):** Added per-period day volatility attributes to detect when classification changes are meaningful: + - `day_volatility_%`: Percentage spread (span/avg × 100) - `day_price_min`, `day_price_max`, `day_price_span`: Daily price range (ct/øre) Automations can check volatility before acting: + ```yaml condition: - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} ``` Low volatility (< 15%) means classification changes are less economically significant. @@ -1224,24 +1326,25 @@ Low volatility (< 15%) means classification changes are less economically signif **Alternative Approaches Rejected:** 1. **Use period start day for all intervals** - - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day - - Rejected: Violates relative evaluation principle + - Problem: Mathematically incorrect - lends cheap day's criteria to expensive day + - Rejected: Violates relative evaluation principle 2. **Adjust flex/distance at midnight** - - Problem: Complex, unpredictable, hides market reality - - Rejected: Users should understand price context, not have it hidden + - Problem: Complex, unpredictable, hides market reality + - Rejected: Users should understand price context, not have it hidden 3. **Split at midnight always** - - Problem: Artificially fragments natural periods - - Rejected: Worse user experience + - Problem: Artificially fragments natural periods + - Rejected: Worse user experience 4. **Use next day's reference after midnight** - - Problem: Period criteria inconsistent across duration - - Rejected: Confusing and unpredictable + - Problem: Period criteria inconsistent across duration + - Rejected: Confusing and unpredictable **Status:** Per-day evaluation is intentional design prioritizing mathematical correctness. **See Also:** + - User documentation: `docs/user/docs/period-calculation.md` → "Midnight Price Classification Changes" - Implementation: `coordinator/period_handlers/period_building.py` (line ~126: `ref_date = date_key`) - Attributes: `coordinator/period_handlers/period_statistics.py` (day volatility calculation) diff --git a/docs/developer/versioned_docs/version-v0.30.0/recorder-optimization.md b/docs/developer/versioned_docs/version-v0.30.0/recorder-optimization.md index 50cc494..f56ea9c 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/recorder-optimization.md +++ b/docs/developer/versioned_docs/version-v0.30.0/recorder-optimization.md @@ -29,6 +29,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ``` **Key Points:** + - Must be a **class attribute** (not instance attribute) - Use `frozenset` for immutability and performance - Applied automatically by Home Assistant's Recorder component @@ -40,6 +41,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `description`, `usage_tips` **Reason:** Static, large text strings (100-500 chars each) that: + - Never change or change very rarely - Don't provide analytical value in history - Consume significant database space when recorded every state change @@ -50,6 +52,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 2. Large Nested Structures **Attributes:** + - `periods` (binary_sensor) - Array of all period summaries - `data` (chart_data_export) - Complete price data arrays - `trend_attributes` - Detailed trend analysis @@ -58,6 +61,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): - `volatility_attributes` - Detailed volatility breakdown **Reason:** Complex nested data structures that are: + - Serialized to JSON for storage (expensive) - Create large database rows (2-20 KB each) - Slow down history queries @@ -66,20 +70,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~10-30 KB saved per state change for affected sensors **Example - periods array:** + ```json { - "periods": [ - { - "start": "2025-12-07T06:00:00+01:00", - "end": "2025-12-07T08:00:00+01:00", - "duration_minutes": 120, - "price_mean": 18.5, - "price_median": 18.3, - "price_min": 17.2, - "price_max": 19.8, - // ... 10+ more attributes × 10-20 periods - } - ] + "periods": [ + { + "start": "2025-12-07T06:00:00+01:00", + "end": "2025-12-07T08:00:00+01:00", + "duration_minutes": 120, + "price_mean": 18.5, + "price_median": 18.3, + "price_min": 17.2, + "price_max": 19.8 + // ... 10+ more attributes × 10-20 periods + } + ] } ``` @@ -88,6 +93,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status` **Reason:** + - Change every update cycle (every 15 minutes or more frequently) - Don't provide long-term analytical value - Create state changes even when core values haven't changed @@ -103,6 +109,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max` **Reason:** + - Configuration values that rarely change - Wastes space when recorded repeatedly - Can be derived from other attributes or from entity state @@ -114,6 +121,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `timestamp`, `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error` **Reason:** + - `timestamp` is the rounded-quarter reference time used at the moment of the state write — it's stale as soon as the next update fires and has no analytical value in history - `next_api_poll`, `next_midnight_turnover` etc. are only relevant at the moment of reading; they're superseded by the next update - Similar to `entity_picture` in HA core image entities @@ -129,6 +137,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%` **Reason:** + - Detailed technical information not needed for historical analysis - Only useful for debugging during active development - Boolean `relaxation_active` is kept for high-level analysis @@ -140,6 +149,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` **Reason:** + - Can be calculated from other attributes - Redundant information - Doesn't add analytical value to history @@ -153,21 +163,26 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): These attributes **remain in history** because they provide essential analytical value: ### Time-Series Core + - All price values - Core sensor states (the entity's `native_value` is always recorded separately) ### Diagnostics & Tracking + - `cache_age_minutes` - Numeric value for diagnostics tracking over time - `updates_today` - Tracking API usage patterns ### Data Completeness + - `interval_count`, `intervals_available` - Data completeness metrics - `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status ### Period Data + - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics ### High-Level Status + - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) ## Expected Database Impact @@ -175,6 +190,7 @@ These attributes **remain in history** because they provide essential analytical ### Space Savings **Per state change:** + - Before: ~3-8 KB average - After: ~0.5-1.5 KB average - **Reduction: 60-85%** @@ -196,6 +212,7 @@ These attributes **remain in history** because they provide essential analytical ### Real-World Impact For a typical installation with: + - 80+ sensors - Updates every 15 minutes - ~10 sensors updating every minute @@ -207,14 +224,14 @@ For a typical installation with: ## Implementation Files - **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py` - - Class: `TibberPricesSensor` - - 46 attributes excluded + - Class: `TibberPricesSensor` + - 46 attributes excluded - **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py` - - Class: `TibberPricesBinarySensor` - - 29 attributes excluded + - Class: `TibberPricesBinarySensor` + - 29 attributes excluded -## When to Update _unrecorded_attributes +## When to Update \_unrecorded_attributes ### Add to Exclusion List When: @@ -236,24 +253,24 @@ For a typical installation with: When adding a new attribute, ask: 1. **Will this be useful in history queries 1 week from now?** - - No → Exclude - - Yes → Keep + - No → Exclude + - Yes → Keep 2. **Can this be calculated from other recorded attributes?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 3. **Is this primarily for current UI display?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 4. **Does this change frequently without indicating state change?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep 5. **Is this larger than 100 bytes and not essential for analysis?** - - Yes → Exclude - - No → Keep + - Yes → Exclude + - No → Keep ## Testing @@ -265,6 +282,7 @@ After modifying `_unrecorded_attributes`: 4. **Confirm excluded attributes** don't appear in new state writes **SQL Query to check attribute presence:** + ```sql SELECT state_id, @@ -298,11 +316,11 @@ This makes `state_class=TOTAL` on many sensors the primary cause of long-term da For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_class` values are valid: -| `state_class` | Statistics written | Frontend effect | -|---|---|---| -| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page | -| `None` | ❌ No | States timeline only (History panel, "Show More") | -| `MEASUREMENT` | ❌ Blocked by hassfest | — | +| `state_class` | Statistics written | Frontend effect | +| ------------- | ------------------------- | ------------------------------------------------- | +| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page | +| `None` | ❌ No | States timeline only (History panel, "Show More") | +| `MEASUREMENT` | ❌ Blocked by hassfest | — | `MEASUREMENT` causes a hassfest validation error for MONETARY sensors, leaving only `TOTAL` or `None`. @@ -310,19 +328,21 @@ For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_clas Only 3 of 26 MONETARY sensors keep `state_class=TOTAL` — those where long-term history is genuinely useful: -| Sensor | Reason | -|---|---| -| `current_interval_price` | Long-term price trend (weeks/months) | -| `current_interval_price_base` | Required for Energy Dashboard | -| `average_price_today` | Seasonal daily average tracking | +| Sensor | Reason | +| ----------------------------- | ------------------------------------ | +| `current_interval_price` | Long-term price trend (weeks/months) | +| `current_interval_price_base` | Required for Energy Dashboard | +| `average_price_today` | Seasonal daily average tracking | All other 23 MONETARY sensors use `state_class=None`: + - Forecast/future sensors (`next_avg_*h`) - Daily snapshots (`lowest/highest_price_today/tomorrow`) - Rolling windows (`trailing/leading_24h_*`) - Next/previous interval sensors **Effect of `state_class=None`:** + - ✅ Short-term state history (States timeline, ~10 days) still works normally - ✅ Templates, automations, and attributes are unaffected - ❌ Statistics line-chart removed from entity detail page for these sensors @@ -331,6 +351,7 @@ All other 23 MONETARY sensors use `state_class=None`: ### Expected Impact Going from 26 → 3 sensors writing to the statistics tables: + - **~88% reduction** in statistics table writes - Prevents the primary cause of long-term database bloat - Existing statistics data is retained (only new writes stop) @@ -339,10 +360,10 @@ Going from 26 → 3 sensors writing to the statistics tables: These are two independent mechanisms targeting different tables: -| Mechanism | Table affected | Purged? | Controls | -|---|---|---|---| -| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write | -| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all | +| Mechanism | Table affected | Purged? | Controls | +| ------------------------ | ------------------------------------- | ----------- | ----------------------------------------------- | +| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write | +| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all | Both optimizations work together. `_unrecorded_attributes` reduces the size of each state write; `state_class=None` eliminates an entire category of unbounded writes. diff --git a/docs/developer/versioned_docs/version-v0.30.0/refactoring-guide.md b/docs/developer/versioned_docs/version-v0.30.0/refactoring-guide.md index 14cbf14..eda9f49 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/refactoring-guide.md +++ b/docs/developer/versioned_docs/version-v0.30.0/refactoring-guide.md @@ -8,22 +8,22 @@ Not every code change needs a detailed plan. Create a refactoring plan when: 🔴 **Major changes requiring planning:** -- Splitting modules into packages (>5 files affected, >500 lines moved) -- Architectural changes (new packages, module restructuring) -- Breaking changes (API changes, config format migrations) +- Splitting modules into packages (>5 files affected, >500 lines moved) +- Architectural changes (new packages, module restructuring) +- Breaking changes (API changes, config format migrations) 🟡 **Medium changes that might benefit from planning:** -- Complex features with multiple moving parts -- Changes affecting many files (>3 files, unclear best approach) -- Refactorings with unclear scope +- Complex features with multiple moving parts +- Changes affecting many files (>3 files, unclear best approach) +- Refactorings with unclear scope 🟢 **Small changes - no planning needed:** -- Bug fixes (straightforward, `<`100 lines) -- Small features (`<`3 files, clear approach) -- Documentation updates -- Cosmetic changes (formatting, renaming) +- Bug fixes (straightforward, `<`100 lines) +- Small features (`<`3 files, clear approach) +- Documentation updates +- Cosmetic changes (formatting, renaming) ## The Planning Process @@ -51,34 +51,34 @@ Every planning document should include: ## Problem Statement -- What's the issue? -- Why does it need fixing? -- Current pain points +- What's the issue? +- Why does it need fixing? +- Current pain points ## Proposed Solution -- High-level approach -- File structure (before/after) -- Module responsibilities +- High-level approach +- File structure (before/after) +- Module responsibilities ## Migration Strategy -- Phase-by-phase breakdown -- File lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Dependencies between phases -- Testing checkpoints +- Phase-by-phase breakdown +- File lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Dependencies between phases +- Testing checkpoints ## Risks & Mitigation -- What could go wrong? -- How to prevent it? -- Rollback strategy +- What could go wrong? +- How to prevent it? +- Rollback strategy ## Success Criteria -- Measurable improvements -- Testing requirements -- Verification steps +- Measurable improvements +- Testing requirements +- Verification steps ``` See `planning/README.md` for detailed template explanation. @@ -87,19 +87,19 @@ See `planning/README.md` for detailed template explanation. Since `planning/` is git-ignored: -- Draft multiple versions -- Get AI assistance without commit pressure -- Refine until the plan is solid -- No need to clean up intermediate versions +- Draft multiple versions +- Get AI assistance without commit pressure +- Refine until the plan is solid +- No need to clean up intermediate versions ### 4. Implementation Phase Once plan is approved: -- Follow the phases defined in the plan -- Test after each phase (don't skip!) -- Update plan if issues discovered -- Track progress through phase status +- Follow the phases defined in the plan +- Test after each phase (don't skip!) +- Update plan if issues discovered +- Track progress through phase status ### 5. After Completion @@ -134,13 +134,13 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Before:** -- `sensor.py` - 2,574 lines, hard to navigate +- `sensor.py` - 2,574 lines, hard to navigate **After:** -- `sensor/` package with 5 focused modules -- Each module `<`800 lines -- Clear separation of concerns +- `sensor/` package with 5 focused modules +- Each module `<`800 lines +- Clear separation of concerns **Process:** @@ -153,10 +153,10 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: **Key learnings:** -- Temporary `_impl.py` files avoid Python package conflicts -- Test after EVERY phase (don't accumulate changes) -- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) -- Phase-by-phase approach enables safe rollback +- Temporary `_impl.py` files avoid Python package conflicts +- Test after EVERY phase (don't accumulate changes) +- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME) +- Phase-by-phase approach enables safe rollback **Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure. @@ -166,11 +166,11 @@ The **sensor/ package refactoring** (Nov 2025) is a successful example: Breaking refactorings into phases: -- ✅ Enables testing after each change (catch bugs early) -- ✅ Allows rollback to last good state -- ✅ Makes progress visible -- ✅ Reduces cognitive load (focus on one thing) -- ❌ Takes more time (but worth it!) +- ✅ Enables testing after each change (catch bugs early) +- ✅ Allows rollback to last good state +- ✅ Makes progress visible +- ✅ Reduces cognitive load (focus on one thing) +- ❌ Takes more time (but worth it!) ### Phase Structure @@ -191,8 +191,8 @@ Each phase should: **File Lifecycle**: -- ✨ CREATE `sensor/helpers.py` (utility functions) -- ✏️ MODIFY `sensor/core.py` (import from helpers.py) +- ✨ CREATE `sensor/helpers.py` (utility functions) +- ✏️ MODIFY `sensor/core.py` (import from helpers.py) **Steps**: @@ -205,10 +205,10 @@ Each phase should: **Success criteria**: -- ✅ All pure functions moved -- ✅ `./scripts/lint-check` passes -- ✅ HA starts successfully -- ✅ All entities work correctly +- ✅ All pure functions moved +- ✅ `./scripts/lint-check` passes +- ✅ HA starts successfully +- ✅ All entities work correctly ``` ## Testing Strategy @@ -238,13 +238,13 @@ Minimum testing checklist: After completing all phases: -- Test all entities (sensors, binary sensors) -- Test configuration flow (add/modify/remove) -- Test options flow (change settings) -- Test services (custom service calls) -- Test error handling (disconnect API, invalid data) -- Test caching (restart HA, verify cache loads) -- Test time-based updates (quarter-hour refresh) +- Test all entities (sensors, binary sensors) +- Test configuration flow (add/modify/remove) +- Test options flow (change settings) +- Test services (custom service calls) +- Test error handling (disconnect API, invalid data) +- Test caching (restart HA, verify cache loads) +- Test time-based updates (quarter-hour refresh) ## Common Pitfalls @@ -286,21 +286,21 @@ This project uses AI heavily (GitHub Copilot, Claude). The planning process supp **AI reads from:** -- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) -- `docs/development/` - Human-readable guides (human-focused) -- `planning/` - Active refactoring plans (shared context) +- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused) +- `docs/development/` - Human-readable guides (human-focused) +- `planning/` - Active refactoring plans (shared context) **AI updates:** -- `AGENTS.md` - When patterns change -- `planning/*.md` - During refactoring implementation -- `docs/development/` - After successful completion +- `AGENTS.md` - When patterns change +- `planning/*.md` - During refactoring implementation +- `docs/development/` - After successful completion **Why separate AGENTS.md and docs/development/?** -- `AGENTS.md`: Technical, comprehensive, AI-optimized -- `docs/development/`: Practical, focused, human-optimized -- Both stay in sync but serve different audiences +- `AGENTS.md`: Technical, comprehensive, AI-optimized +- `docs/development/`: Practical, focused, human-optimized +- Both stay in sync but serve different audiences See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.30.0/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance. @@ -308,16 +308,16 @@ See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.30.0/AG ### Planning Directory -- `planning/` - Git-ignored workspace for drafts -- `planning/README.md` - Detailed planning documentation -- `planning/*.md` - Active refactoring plans +- `planning/` - Git-ignored workspace for drafts +- `planning/README.md` - Detailed planning documentation +- `planning/*.md` - Active refactoring plans ### Example Plans -- `docs/development/module-splitting-plan.md` - ✅ Completed, archived -- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) -- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) -- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) +- `docs/development/module-splitting-plan.md` - ✅ Completed, archived +- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules) +- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules) +- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity) ### Helper Scripts @@ -341,21 +341,21 @@ Simple rule: If you can't describe the entire change in 3 sentences, create a pl Good plan level: -- Lists all files affected (CREATE/MODIFY/DELETE) -- Defines phases with clear boundaries -- Includes testing strategy -- Estimates time per phase +- Lists all files affected (CREATE/MODIFY/DELETE) +- Defines phases with clear boundaries +- Includes testing strategy +- Estimates time per phase Too detailed: -- Exact code snippets for every change -- Line-by-line instructions +- Exact code snippets for every change +- Line-by-line instructions Too vague: -- "Refactor sensor.py to be better" -- No phase breakdown -- No testing strategy +- "Refactor sensor.py to be better" +- No phase breakdown +- No testing strategy ### Q: What if the plan changes during implementation? @@ -363,9 +363,9 @@ Too vague: If you discover: -- Better approach → Update "Proposed Solution" -- More phases needed → Add to "Migration Strategy" -- New risks → Update "Risks & Mitigation" +- Better approach → Update "Proposed Solution" +- More phases needed → Add to "Migration Strategy" +- New risks → Update "Risks & Mitigation" Document WHY the plan changed (helps future refactorings). @@ -373,9 +373,9 @@ Document WHY the plan changed (helps future refactorings). **A:** No! Use judgment: -- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed -- **Medium changes (unclear scope)**: Write rough outline, refine if needed -- **Large changes (>500 lines, >5 files)**: Full planning process +- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed +- **Medium changes (unclear scope)**: Write rough outline, refine if needed +- **Large changes (>500 lines, >5 files)**: Full planning process ### Q: How do I know when a refactoring is successful? @@ -383,12 +383,12 @@ Document WHY the plan changed (helps future refactorings). Typical criteria: -- ✅ All linting checks pass -- ✅ HA starts without errors -- ✅ All entities functional -- ✅ No regressions (existing features work) -- ✅ Code easier to understand/modify -- ✅ Documentation updated +- ✅ All linting checks pass +- ✅ HA starts without errors +- ✅ All entities functional +- ✅ No regressions (existing features work) +- ✅ Code easier to understand/modify +- ✅ Documentation updated If you can't tick all boxes, the refactoring isn't done. @@ -409,6 +409,6 @@ If you can't tick all boxes, the refactoring isn't done. **Next steps:** -- Read `planning/README.md` for detailed template -- Check `docs/development/module-splitting-plan.md` for real example -- Browse `planning/` for active refactoring plans +- Read `planning/README.md` for detailed template +- Check `docs/development/module-splitting-plan.md` for real example +- Browse `planning/` for active refactoring plans diff --git a/docs/developer/versioned_docs/version-v0.30.0/release-management.md b/docs/developer/versioned_docs/version-v0.30.0/release-management.md index 3893d22..9b6a095 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/release-management.md +++ b/docs/developer/versioned_docs/version-v0.30.0/release-management.md @@ -112,6 +112,7 @@ In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled. **In DevContainer (automatic):** git-cliff is automatically installed when the DevContainer is built: + - **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile) - **git-cliff**: Installed via cargo in `scripts/setup/setup` @@ -120,6 +121,7 @@ Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and **Manual installation (outside DevContainer):** **git-cliff** (template-based): + ```bash # See: https://git-cliff.org/docs/installation @@ -190,13 +192,13 @@ All methods produce GitHub-flavored Markdown with emoji categories: ## 🎯 When to Use Which -| Method | Use Case | Pros | Cons | -|--------|----------|------|------| -| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | -| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | -| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | -| **Local Script** | Testing release notes | Preview before release | Manual process | -| **CI/CD** | After tag push | Fully automatic | Needs tag first | +| Method | Use Case | Pros | Cons | +| --------------------- | --------------------- | ----------------------------- | ------------------------ | +| **Helper Script** | Normal releases | Foolproof, automatic | Requires script | +| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump | +| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization | +| **Local Script** | Testing release notes | Preview before release | Manual process | +| **CI/CD** | After tag push | Fully automatic | Needs tag first | --- @@ -219,6 +221,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. Script bumps manifest.json → commits → creates tag locally 2. You push commit + tag together 3. Release workflow sees tag → generates notes → creates release @@ -242,6 +245,7 @@ git push ``` **What happens:** + 1. You push manifest.json change 2. Auto-Tag workflow detects change → creates tag automatically 3. Release workflow sees new tag → creates release @@ -263,6 +267,7 @@ git push origin main v0.3.0 ``` **What happens:** + 1. You create and push tag manually 2. Release workflow creates release 3. Auto-Tag workflow skips (tag already exists) @@ -282,19 +287,24 @@ git push origin main v0.3.0 ## 🛡️ Safety Features ### 1. **Version Validation** + Both helper script and auto-tag workflow validate version format (X.Y.Z). ### 2. **No Duplicate Tags** + - Helper script checks if tag exists (local + remote) - Auto-tag workflow checks if tag exists before creating ### 3. **Atomic Operations** + Helper script creates commit + tag locally. You decide when to push. ### 4. **Version Bumps Filtered** + Release notes automatically exclude `chore(release): bump version` commits. ### 5. **Rollback Instructions** + Helper script shows how to undo if you change your mind. --- @@ -330,6 +340,7 @@ git push -f origin main v0.3.0 **Auto-tag didn't create tag:** Check workflow runs in GitHub Actions. Common causes: + - Tag already exists remotely - Invalid version format in manifest.json - manifest.json not in the commit that was pushed @@ -348,13 +359,14 @@ Check workflow runs in GitHub Actions. Common causes: ## 💡 Tips 1. **Conventional Commits:** Use proper commit format for best results: - ``` - feat(scope): Add new feature - Detailed description of what changed. + ``` + feat(scope): Add new feature - Impact: Users can now do X and Y. - ``` + Detailed description of what changed. + + Impact: Users can now do X and Y. + ``` 2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions diff --git a/docs/developer/versioned_docs/version-v0.30.0/repairs-system.md b/docs/developer/versioned_docs/version-v0.30.0/repairs-system.md index 0e2beac..36f7314 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/repairs-system.md +++ b/docs/developer/versioned_docs/version-v0.30.0/repairs-system.md @@ -7,6 +7,7 @@ The Tibber Prices integration includes a proactive repair notification system th The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle. **Design Principles:** + - **Proactive**: Detect issues before they become critical - **User-friendly**: Clear explanations with actionable guidance - **Auto-clearing**: Repairs automatically disappear when conditions resolve @@ -19,10 +20,12 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri **Issue ID:** `tomorrow_data_missing_{entry_id}` **When triggered:** + - Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`) - Tomorrow's electricity price data is still not available **When cleared:** + - Tomorrow's data becomes available - Automatically checks on every successful API update @@ -30,6 +33,7 @@ The repairs system is implemented in `coordinator/repairs.py` via the `TibberPri Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work. **Implementation:** + ```python # In coordinator update cycle has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"]) @@ -40,6 +44,7 @@ await self._repair_manager.check_tomorrow_data_availability( ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `warning_hour`: Hour after which warning appears (default: 18) @@ -48,10 +53,12 @@ await self._repair_manager.check_tomorrow_data_availability( **Issue ID:** `rate_limit_exceeded_{entry_id}` **When triggered:** + - Integration encounters 3 or more consecutive rate limit errors (HTTP 429) - Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD` **When cleared:** + - Successful API call completes (no rate limit error) - Error counter resets to 0 @@ -59,6 +66,7 @@ await self._repair_manager.check_tomorrow_data_availability( API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires. **Implementation:** + ```python # In error handler is_rate_limit = ( @@ -74,6 +82,7 @@ await self._repair_manager.clear_rate_limit_tracking() ``` **Translation placeholders:** + - `home_name`: Name of the affected home - `error_count`: Number of consecutive rate limit errors @@ -82,10 +91,12 @@ await self._repair_manager.clear_rate_limit_tracking() **Issue ID:** `home_not_found_{entry_id}` **When triggered:** + - Home configured in this integration is no longer present in Tibber account - Detected during user data refresh (daily check) **When cleared:** + - Home reappears in Tibber account (unlikely - manual cleanup expected) - Integration entry is removed (shutdown cleanup) @@ -93,6 +104,7 @@ await self._repair_manager.clear_rate_limit_tracking() Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed. **Implementation:** + ```python # After user data update home_exists = self._data_fetcher._check_home_exists(home_id) @@ -103,6 +115,7 @@ else: ``` **Translation placeholders:** + - `home_name`: Name of the missing home - `entry_id`: Config entry ID for reference @@ -153,6 +166,7 @@ Each repair type maintains internal state to avoid redundant operations: ### Lifecycle Integration **Coordinator Initialization:** + ```python self._repair_manager = TibberPricesRepairManager( hass=hass, @@ -162,6 +176,7 @@ self._repair_manager = TibberPricesRepairManager( ``` **Update Cycle Integration:** + ```python # Success path - check conditions if result and "priceInfo" in result: @@ -178,6 +193,7 @@ if is_rate_limit: ``` **Shutdown Cleanup:** + ```python async def async_shutdown(self) -> None: """Shut down coordinator and clean up.""" @@ -196,24 +212,27 @@ Repairs use Home Assistant's standard translation system. Translations are defin - `/translations/sv.json` **Structure:** + ```json { - "issues": { - "tomorrow_data_missing": { - "title": "Tomorrow's price data missing for {home_name}", - "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + "issues": { + "tomorrow_data_missing": { + "title": "Tomorrow's price data missing for {home_name}", + "description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2" + } } - } } ``` ## Home Assistant Integration Repairs appear in: + - **Settings → System → Repairs** (main repairs panel) - **Notifications** (bell icon in UI shows repair count) Repair properties: + - **`is_fixable=False`**: No automated fix available (user action required) - **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical) - **`translation_key`**: References `issues.{key}` in translation files @@ -228,6 +247,7 @@ Repair properties: 4. When tomorrow data arrives (next API fetch), repair clears **Manual trigger:** + ```python # Temporarily set warning hour to current hour for testing TOMORROW_DATA_WARNING_HOUR = datetime.now().hour @@ -240,6 +260,7 @@ TOMORROW_DATA_WARNING_HOUR = datetime.now().hour 3. Successful API call clears the repair **Manual test:** + - Reduce API polling interval to trigger rate limiting - Or temporarily return HTTP 429 in API client @@ -263,6 +284,7 @@ To add a new repair type: 7. **Document** in this file **Example template:** + ```python async def check_new_condition(self, *, param: bool) -> None: """Check new condition and create/clear repair.""" diff --git a/docs/developer/versioned_docs/version-v0.30.0/setup.md b/docs/developer/versioned_docs/version-v0.30.0/setup.md index 3615143..65ba99b 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/setup.md +++ b/docs/developer/versioned_docs/version-v0.30.0/setup.md @@ -4,9 +4,9 @@ ## Prerequisites -- VS Code with Dev Container support -- Docker installed and running -- GitHub account (for Tibber API token) +- VS Code with Dev Container support +- Docker installed and running +- GitHub account (for Tibber API token) ## Quick Setup @@ -26,11 +26,11 @@ code . The DevContainer includes: -- Python 3.13 with `.venv` at `/home/vscode/.venv/` -- `uv` package manager (fast, modern Python tooling) -- Home Assistant development dependencies -- Ruff linter/formatter -- Git, GitHub CLI, Node.js, Rust toolchain +- Python 3.13 with `.venv` at `/home/vscode/.venv/` +- `uv` package manager (fast, modern Python tooling) +- Home Assistant development dependencies +- Ruff linter/formatter +- Git, GitHub CLI, Node.js, Rust toolchain ## Running the Integration diff --git a/docs/developer/versioned_docs/version-v0.30.0/testing.md b/docs/developer/versioned_docs/version-v0.30.0/testing.md index e4787c6..42f0cfd 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/testing.md +++ b/docs/developer/versioned_docs/version-v0.30.0/testing.md @@ -13,10 +13,10 @@ Before running tests or committing changes, validate the integration structure: This lightweight script checks: -- ✓ `config_flow.py` exists -- ✓ `manifest.json` is valid JSON with required fields -- ✓ Translation files have valid JSON syntax -- ✓ All Python files compile without syntax errors +- ✓ `config_flow.py` exists +- ✓ `manifest.json` is valid JSON with required fields +- ✓ Translation files have valid JSON syntax +- ✓ All Python files compile without syntax errors **Note:** Full hassfest validation runs in GitHub Actions on push. @@ -42,10 +42,10 @@ pytest --cov=custom_components.tibber_prices tests/ Then test in Home Assistant UI: -- Configuration flow -- Sensor states and attributes -- Services -- Translation strings +- Configuration flow +- Sensor states and attributes +- Services +- Translation strings ## Test Guidelines diff --git a/docs/developer/versioned_docs/version-v0.30.0/timer-architecture.md b/docs/developer/versioned_docs/version-v0.30.0/timer-architecture.md index f9b8162..ea05a87 100644 --- a/docs/developer/versioned_docs/version-v0.30.0/timer-architecture.md +++ b/docs/developer/versioned_docs/version-v0.30.0/timer-architecture.md @@ -10,11 +10,11 @@ This document explains the timer/scheduler system in the Tibber Prices integrati The integration uses **three independent timer mechanisms** for different purposes: -| Timer | Type | Interval | Purpose | Trigger Method | -|-------|------|----------|---------|----------------| -| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | -| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | -| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | +| Timer | Type | Interval | Purpose | Trigger Method | +| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- | +| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` | +| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` | +| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` | **Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**. @@ -27,6 +27,7 @@ The integration uses **three independent timer mechanisms** for different purpos **Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes` **What it is:** + - HA provides this timer system automatically when you inherit from `DataUpdateCoordinator` - Triggers `_async_update_data()` method every 15 minutes - **Not** synchronized to clock boundaries (each installation has different start time) @@ -53,16 +54,19 @@ async def _async_update_data(self) -> TibberPricesData: ``` **Load Distribution:** + - Each HA installation starts Timer #1 at different times → natural distribution - Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API - Result: API load spread over ~30 minutes instead of all at once **Midnight Coordination:** + - Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects) - If midnight turnover needed → performs it and returns early - Timer #2 will see turnover already done and skip gracefully **Why we use HA's timer:** + - Automatic restart after HA restart - Built-in retry logic for temporary failures - Standard HA integration pattern @@ -79,6 +83,7 @@ async def _async_update_data(self) -> TibberPricesData: **Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll** **Problem it solves:** + - Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48) - Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes - Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13 @@ -100,22 +105,26 @@ async def _handle_quarter_hour_refresh(self, now: datetime) -> None: ``` **Smart Boundary Tolerance:** + - Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance - HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval) - HA restart at 14:59:30 → stays at 14:45:00 (shows current interval) - See [Architecture](./architecture.md#3-quarter-hour-precision) for details **Absolute Time Scheduling:** + - `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...) - NOT relative delays ("in 15 minutes") - If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates) **Which entities listen:** + - All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`) - Binary sensors that check "is now in period?" (e.g., `best_price_period_active`) - ~50-60 entities out of 120+ total **Why custom timer:** + - HA's built-in coordinator doesn't support exact boundary timing - We need **absolute time** triggers, not periodic intervals - Allows fast entity updates without expensive data transformation @@ -140,6 +149,7 @@ async def _handle_minute_refresh(self, now: datetime) -> None: ``` **Which entities listen:** + - `best_price_remaining_minutes` - Countdown timer - `peak_price_remaining_minutes` - Countdown timer - `best_price_progress` - Progress bar (0-100%) @@ -147,11 +157,13 @@ async def _handle_minute_refresh(self, now: datetime) -> None: - ~10 entities total **Why custom timer:** + - Users want smooth countdowns (not jumping 15 minutes at a time) - Progress bars need minute-by-minute updates - Very lightweight (no data processing, just state recalculation) **Why NOT every second:** + - Minute precision sufficient for countdown UX - Reduces CPU load (60× fewer updates than seconds) - Home Assistant best practice (avoid sub-minute updates) @@ -194,6 +206,7 @@ class ListenerManager: ``` **Why this pattern:** + - Decouples timer logic from entity logic - One timer can notify many entities efficiently - Entities can unregister when removed (cleanup) @@ -279,11 +292,13 @@ class ListenerManager: ### Reason 1: Load Distribution on Tibber API If all installations used synchronized timers: + - ❌ Everyone fetches at 13:00:00 → Tibber API overload - ❌ Everyone fetches at 14:00:00 → Tibber API overload - ❌ "Thundering herd" problem With HA's unsynchronized timer: + - ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ... - ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ... - ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ... @@ -316,6 +331,7 @@ def _should_update_price_data(self) -> str: **Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data. **API fetch only when:** + - Tomorrow data missing/invalid (after 13:00) - Cache expired (midnight turnover) - Explicit user refresh @@ -339,6 +355,7 @@ def _should_update_price_data(self) -> str: ## Performance Characteristics ### Timer #1 (DataUpdateCoordinator) + - **Triggers:** Every 15 minutes (unsynchronized) - **Fast path:** ~2ms (cache check, return existing data) - **Slow path:** ~600ms (API fetch + transform + calculate) @@ -346,12 +363,14 @@ def _should_update_price_data(self) -> str: - **API calls:** ~1-2 times/day (cached otherwise) ### Timer #2 (Quarter-Hour Refresh) + - **Triggers:** 96 times/day (exact boundaries) - **Processing:** ~5ms (notify 60 entities) - **No API calls:** Uses cached/transformed data - **No transformation:** Just entity state updates ### Timer #3 (Minute Refresh) + - **Triggers:** 1440 times/day (every minute) - **Processing:** ~1ms (notify 10 entities) - **No API calls:** No data processing at all @@ -393,16 +412,16 @@ _LOGGER.setLevel(logging.DEBUG) ### Common Issues 1. **Timer #2 not triggering:** - - Check: `schedule_quarter_hour_refresh()` called in `__init__`? - - Check: `_quarter_hour_timer_cancel` properly stored? + - Check: `schedule_quarter_hour_refresh()` called in `__init__`? + - Check: `_quarter_hour_timer_cancel` properly stored? 2. **Double updates at midnight:** - - Should NOT happen (atomic coordination) - - Check: Both timers use same date comparison logic? + - Should NOT happen (atomic coordination) + - Check: Both timers use same date comparison logic? 3. **API overload:** - - Check: Random delay working? (0-30s jitter on tomorrow check) - - Check: Cache validation logic correct? + - Check: Random delay working? (0-30s jitter on tomorrow check) + - Check: Cache validation logic correct? --- @@ -417,17 +436,20 @@ _LOGGER.setLevel(logging.DEBUG) ## Summary **Three independent timers:** + 1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed) 2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always) 3. **Timer #3** (Custom, every minute) → Countdown/progress (always) **Key insights:** + - Timer #1 unsynchronized = good (load distribution on API) - Timer #2 synchronized = good (user sees correct data immediately) - Timer #3 synchronized = good (smooth countdown UX) - All three coordinate gracefully (atomic midnight checks, no conflicts) **"Listener" terminology:** + - Timer = mechanism that triggers - Listener = callback that gets called - Observer pattern = entities register, coordinator notifies diff --git a/docs/developer/versioned_sidebars/version-v0.21.0-sidebars.json b/docs/developer/versioned_sidebars/version-v0.21.0-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.21.0-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.21.0-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/developer/versioned_sidebars/version-v0.22.0-sidebars.json b/docs/developer/versioned_sidebars/version-v0.22.0-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.22.0-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.22.0-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/developer/versioned_sidebars/version-v0.22.1-sidebars.json b/docs/developer/versioned_sidebars/version-v0.22.1-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.22.1-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.22.1-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/developer/versioned_sidebars/version-v0.23.0-sidebars.json b/docs/developer/versioned_sidebars/version-v0.23.0-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.23.0-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.23.0-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/developer/versioned_sidebars/version-v0.23.1-sidebars.json b/docs/developer/versioned_sidebars/version-v0.23.1-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.23.1-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.23.1-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/developer/versioned_sidebars/version-v0.24.0-sidebars.json b/docs/developer/versioned_sidebars/version-v0.24.0-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.24.0-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.24.0-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/developer/versioned_sidebars/version-v0.27.0-sidebars.json b/docs/developer/versioned_sidebars/version-v0.27.0-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.27.0-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.27.0-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/developer/versioned_sidebars/version-v0.28.0-sidebars.json b/docs/developer/versioned_sidebars/version-v0.28.0-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.28.0-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.28.0-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/developer/versioned_sidebars/version-v0.29.0-sidebars.json b/docs/developer/versioned_sidebars/version-v0.29.0-sidebars.json index 9508a86..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.29.0-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.29.0-sidebars.json @@ -1,61 +1,56 @@ { - "tutorialSidebar": [ - "intro", - { - "type": "category", - "label": "🏗️ Architecture", - "items": [ - "architecture", - "timer-architecture", - "caching-strategy", - "api-reference" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "💻 Development", - "items": [ - "setup", - "coding-guidelines", - "critical-patterns", - "repairs-system", - "debugging" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "📐 Advanced Topics", - "items": [ - "period-calculation-theory", - "refactoring-guide", - "performance", - "recorder-optimization" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "📝 Contributing", - "items": [ - "contributing" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], - "collapsible": true, - "collapsed": false - } - ] + "tutorialSidebar": [ + "intro", + { + "type": "category", + "label": "🏗️ Architecture", + "items": [ + "architecture", + "timer-architecture", + "caching-strategy", + "api-reference" + ], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "💻 Development", + "items": [ + "setup", + "coding-guidelines", + "critical-patterns", + "repairs-system", + "debugging" + ], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "📐 Advanced Topics", + "items": [ + "period-calculation-theory", + "refactoring-guide", + "performance", + "recorder-optimization" + ], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "📝 Contributing", + "items": ["contributing"], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "🚀 Release", + "items": ["release-management", "testing"], + "collapsible": true, + "collapsed": false + } + ] } diff --git a/docs/developer/versioned_sidebars/version-v0.30.0-sidebars.json b/docs/developer/versioned_sidebars/version-v0.30.0-sidebars.json index ec22ed1..b6727fd 100644 --- a/docs/developer/versioned_sidebars/version-v0.30.0-sidebars.json +++ b/docs/developer/versioned_sidebars/version-v0.30.0-sidebars.json @@ -41,19 +41,14 @@ { "type": "category", "label": "📝 Contributing", - "items": [ - "contributing" - ], + "items": ["contributing"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🚀 Release", - "items": [ - "release-management", - "testing" - ], + "items": ["release-management", "testing"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_docs/version-v0.21.0/actions.md b/docs/user/versioned_docs/version-v0.21.0/actions.md index b71a2aa..018acc6 100644 --- a/docs/user/versioned_docs/version-v0.21.0/actions.md +++ b/docs/user/versioned_docs/version-v0.21.0/actions.md @@ -12,13 +12,13 @@ You can still call them from automations, scripts, and dashboards the same way a **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Major (EUR/NOK) or minor (ct/øre) units +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Major (EUR/NOK) or minor (ct/øre) units **Basic Example:** @@ -50,14 +50,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| Parameter | Description | Default | +| ------------------ | ------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | | `subunit_currency` | Return prices in ct/øre instead of EUR/NOK | `false` | -| `round_decimals` | Decimal places (0-10) | 4 (major) or 2 (minor) | +| `round_decimals` | Decimal places (0-10) | 4 (major) or 2 (minor) | **Rolling Window Mode:** @@ -73,8 +73,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -123,6 +124,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -140,9 +142,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -150,9 +152,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -185,7 +187,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -193,7 +195,7 @@ response_variable: config type: custom:config-template-card entities: - sensor.tibber_home_tomorrow_data - - sensor.tibber_home_chart_metadata # For dynamic Y-axis + - sensor.tibber_home_chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -210,6 +212,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -249,10 +252,10 @@ If you're still using the `sensor.tibber_home_chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.21.0/automation-examples.md b/docs/user/versioned_docs/version-v0.21.0/automation-examples.md index f7a2705..d8300f7 100644 --- a/docs/user/versioned_docs/version-v0.21.0/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.21.0/automation-examples.md @@ -6,10 +6,10 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- @@ -29,31 +29,32 @@ On days with low price variation (< 15% volatility), the difference between "che ```yaml automation: - - alias: "Dishwasher - Best Price (High Volatility Only)" - description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only act if volatility > 15% (meaningful savings) - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Optional: Ensure dishwasher is idle and door closed - - condition: state - entity_id: binary_sensor.dishwasher_door - state: "off" - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher_smart_plug - - service: notify.mobile_app - data: - message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" + - alias: "Dishwasher - Best Price (High Volatility Only)" + description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only act if volatility > 15% (meaningful savings) + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Optional: Ensure dishwasher is idle and door closed + - condition: state + entity_id: binary_sensor.dishwasher_door + state: "off" + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher_smart_plug + - service: notify.mobile_app + data: + message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" ``` **Why this works:** + - On high-volatility days (e.g., 25% span), Best Price periods save 5-10 ct/kWh - On low-volatility days (e.g., 8% span), savings are only 1-2 ct/kWh - User can manually start dishwasher on low-volatility days without automation interference @@ -64,33 +65,34 @@ Instead of relying on relative classification, check if the absolute price is ch ```yaml automation: - - alias: "Water Heater - Cheap Enough" - description: "Heat water when price is below absolute threshold, regardless of period classification" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Absolute threshold: Only run if < 20 ct/kWh - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 20 - # Optional: Check water temperature - - condition: numeric_state - entity_id: sensor.water_heater_temperature - below: 55 # Only heat if below 55°C - action: - - service: switch.turn_on - target: - entity_id: switch.water_heater - - delay: - hours: 2 # Heat for 2 hours - - service: switch.turn_off - target: - entity_id: switch.water_heater + - alias: "Water Heater - Cheap Enough" + description: "Heat water when price is below absolute threshold, regardless of period classification" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Absolute threshold: Only run if < 20 ct/kWh + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 20 + # Optional: Check water temperature + - condition: numeric_state + entity_id: sensor.water_heater_temperature + below: 55 # Only heat if below 55°C + action: + - service: switch.turn_on + target: + entity_id: switch.water_heater + - delay: + hours: 2 # Heat for 2 hours + - service: switch.turn_off + target: + entity_id: switch.water_heater ``` **Why this works:** + - Period classification can flip at midnight on low-volatility days - Absolute threshold (20 ct/kWh) is stable across midnight boundary - User sets their own "cheap enough" price based on local rates @@ -101,40 +103,41 @@ Most robust approach: Check both volatility and absolute price: ```yaml automation: - - alias: "EV Charging - Smart Strategy" - description: "Charge EV using volatility-aware logic" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check battery level - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - # Strategy: High volatility OR cheap enough - - condition: or - conditions: - # Path 1: High volatility day - trust period classification + - alias: "EV Charging - Smart Strategy" + description: "Charge EV using volatility-aware logic" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check battery level - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Path 2: Low volatility but price is genuinely cheap - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 18 - action: - - service: switch.turn_on - target: - entity_id: switch.ev_charger - - service: notify.mobile_app - data: - message: > - EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh - (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) + entity_id: sensor.ev_battery_level + below: 80 + # Strategy: High volatility OR cheap enough + - condition: or + conditions: + # Path 1: High volatility day - trust period classification + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Path 2: Low volatility but price is genuinely cheap + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 18 + action: + - service: switch.turn_on + target: + entity_id: switch.ev_charger + - service: notify.mobile_app + data: + message: > + EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh + (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) ``` **Why this works:** + - On high-volatility days (> 15%): Trust the Best Price classification - On low-volatility days (< 15%): Only charge if price is actually cheap (< 18 ct/kWh) - Handles midnight flips gracefully: Continues charging if price stays cheap @@ -145,51 +148,52 @@ Prevent automations from stopping mid-cycle when a period flips at midnight: ```yaml automation: - - alias: "Washing Machine - Complete Cycle" - description: "Start washing machine during Best Price, ignore midnight flips" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only start if washing machine is idle - - condition: state - entity_id: sensor.washing_machine_state - state: "idle" - # And volatility is meaningful - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - action: - - service: button.press - target: - entity_id: button.washing_machine_eco_program - # Create input_boolean to track active cycle - - service: input_boolean.turn_on - target: - entity_id: input_boolean.washing_machine_auto_started + - alias: "Washing Machine - Complete Cycle" + description: "Start washing machine during Best Price, ignore midnight flips" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only start if washing machine is idle + - condition: state + entity_id: sensor.washing_machine_state + state: "idle" + # And volatility is meaningful + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + action: + - service: button.press + target: + entity_id: button.washing_machine_eco_program + # Create input_boolean to track active cycle + - service: input_boolean.turn_on + target: + entity_id: input_boolean.washing_machine_auto_started - # Separate automation: Clear flag when cycle completes - - alias: "Washing Machine - Cycle Complete" - trigger: - - platform: state - entity_id: sensor.washing_machine_state - to: "finished" - condition: - # Only clear flag if we auto-started it - - condition: state - entity_id: input_boolean.washing_machine_auto_started - state: "on" - action: - - service: input_boolean.turn_off - target: - entity_id: input_boolean.washing_machine_auto_started - - service: notify.mobile_app - data: - message: "Washing cycle complete" + # Separate automation: Clear flag when cycle completes + - alias: "Washing Machine - Cycle Complete" + trigger: + - platform: state + entity_id: sensor.washing_machine_state + to: "finished" + condition: + # Only clear flag if we auto-started it + - condition: state + entity_id: input_boolean.washing_machine_auto_started + state: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.washing_machine_auto_started + - service: notify.mobile_app + data: + message: "Washing cycle complete" ``` **Why this works:** + - Uses `input_boolean` to track auto-started cycles - Won't trigger multiple times if period flips during the 2-3 hour wash cycle - Only triggers on "off" → "on" transitions, not during "on" → "on" continuity @@ -200,25 +204,26 @@ The simplest approach: Use the period's day volatility attribute directly: ```yaml automation: - - alias: "Heat Pump - Smart Heating" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check if the PERIOD'S DAY has meaningful volatility - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} - action: - - service: climate.set_temperature - target: - entity_id: climate.heat_pump - data: - temperature: 22 # Boost temperature during cheap period + - alias: "Heat Pump - Smart Heating" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check if the PERIOD'S DAY has meaningful volatility + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + action: + - service: climate.set_temperature + target: + entity_id: climate.heat_pump + data: + temperature: 22 # Boost temperature during cheap period ``` **Available per-period attributes:** + - `day_volatility_%`: Percentage volatility of the period's day (e.g., 8.2 for 8.2%) - `day_price_min`: Minimum price of the day in minor currency (ct/øre) - `day_price_max`: Maximum price of the day in minor currency (ct/øre) @@ -227,6 +232,7 @@ automation: These attributes are available on both `binary_sensor.tibber_home_best_price_period` and `binary_sensor.tibber_home_peak_price_period`. **Why this works:** + - Each period knows its day's volatility - No need to query separate sensors - Template checks if saving is meaningful (> 15% volatility) @@ -252,9 +258,11 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card ### Prerequisites **Required:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** + - [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -270,8 +278,8 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # or "yesterday", "tomorrow" - level_type: rating_level # or "level" for 5-level view + day: today # or "yesterday", "tomorrow" + level_type: rating_level # or "level" for 5-level view response_variable: apexcharts_config ``` @@ -285,12 +293,13 @@ For a dynamic chart that automatically adapts to data availability: service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: rolling_window # Or omit for same behavior (default) + day: rolling_window # Or omit for same behavior (default) level_type: rating_level response_variable: apexcharts_config ``` **Behavior:** + - **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow - **When tomorrow data not available**: Shows yesterday + today - **Fixed 48h span:** Always shows full 48 hours diff --git a/docs/user/versioned_docs/version-v0.21.0/chart-examples.md b/docs/user/versioned_docs/version-v0.21.0/chart-examples.md index 867620d..0e5c799 100644 --- a/docs/user/versioned_docs/version-v0.21.0/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.21.0/chart-examples.md @@ -8,14 +8,15 @@ This guide showcases the different chart configurations available through the `t The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -29,6 +30,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -43,6 +45,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -59,6 +62,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -73,6 +77,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -80,6 +85,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -93,6 +99,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -107,6 +114,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -114,6 +122,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -155,6 +164,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -166,6 +176,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -191,6 +202,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -205,6 +217,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -217,18 +230,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -243,9 +256,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -260,7 +273,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -292,14 +305,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.21.0/concepts.md b/docs/user/versioned_docs/version-v0.21.0/concepts.md index 2a95bed..1822276 100644 --- a/docs/user/versioned_docs/version-v0.21.0/concepts.md +++ b/docs/user/versioned_docs/version-v0.21.0/concepts.md @@ -21,6 +21,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -33,6 +34,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -53,6 +55,7 @@ This helps you understand if current prices are exceptional or typical. ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -62,6 +65,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.21.0/dashboard-examples.md b/docs/user/versioned_docs/version-v0.21.0/dashboard-examples.md index 0badcb9..795cb74 100644 --- a/docs/user/versioned_docs/version-v0.21.0/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.21.0/dashboard-examples.md @@ -10,13 +10,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor.tibber_home_current_interval_price - name: Current Price - icon: mdi:flash - - entity: sensor.tibber_home_current_interval_rating - name: Price Rating - - entity: sensor.tibber_home_next_interval_price - name: Next Price + - entity: sensor.tibber_home_current_interval_price + name: Current Price + icon: mdi:flash + - entity: sensor.tibber_home_current_interval_rating + name: Price Rating + - entity: sensor.tibber_home_next_interval_price + name: Next Price ``` ## Period Status Cards @@ -26,14 +26,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor.tibber_home_best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor.tibber_home_best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -46,16 +46,16 @@ entity: sensor.tibber_home_current_interval_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -67,19 +67,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor.tibber_home_current_interval_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor.tibber_home_current_interval_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor.tibber_home_best_price_start_time - name: Best Period Starts - - entity: binary_sensor.tibber_home_best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor.tibber_home_best_price_start_time + name: Best Period Starts + - entity: binary_sensor.tibber_home_best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -91,25 +91,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor.tibber_home_current_interval_price - - sensor.tibber_home_current_interval_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor.tibber_home_current_interval_price + - sensor.tibber_home_current_interval_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor.tibber_home_daily_avg_today - - sensor.tibber_home_daily_min_today - - sensor.tibber_home_daily_max_today + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor.tibber_home_daily_avg_today + - sensor.tibber_home_daily_min_today + - sensor.tibber_home_daily_max_today ``` ## Icon Color Integration @@ -119,17 +119,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor.tibber_home_current_interval_price - icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" + - type: entity + entity: sensor.tibber_home_current_interval_price + icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" - - type: entity - entity: binary_sensor.tibber_home_best_price_period - icon_color: green + - type: entity + entity: binary_sensor.tibber_home_best_price_period + icon_color: green - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - icon_color: red + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -142,21 +142,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor.tibber_home_current_interval_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor.tibber_home_current_interval_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor.tibber_home_best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor.tibber_home_best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -166,21 +166,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor.tibber_*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor.tibber_*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.21.0/dynamic-icons.md b/docs/user/versioned_docs/version-v0.21.0/dynamic-icons.md index 7138641..9237b60 100644 --- a/docs/user/versioned_docs/version-v0.21.0/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.21.0/dynamic-icons.md @@ -6,10 +6,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -24,10 +24,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -123,8 +123,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -132,24 +132,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor.tibber_home_best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -157,22 +157,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.21.0/faq.md b/docs/user/versioned_docs/version-v0.21.0/faq.md index 2aa87bf..cba0298 100644 --- a/docs/user/versioned_docs/version-v0.21.0/faq.md +++ b/docs/user/versioned_docs/version-v0.21.0/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -92,6 +100,7 @@ This means **all intervals meet your criteria** (very cheap day!): ### Prices are in wrong currency Integration uses currency from your Tibber subscription: + - EUR → displays in ct/kWh - NOK/SEK → displays in øre/kWh @@ -100,6 +109,7 @@ Cannot be changed (tied to your electricity contract). ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -111,18 +121,18 @@ Cannot be changed (tied to your electricity contract). ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -133,19 +143,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.21.0/glossary.md b/docs/user/versioned_docs/version-v0.21.0/glossary.md index 66171e5..a245435 100644 --- a/docs/user/versioned_docs/version-v0.21.0/glossary.md +++ b/docs/user/versioned_docs/version-v0.21.0/glossary.md @@ -100,6 +100,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.21.0/icon-colors.md b/docs/user/versioned_docs/version-v0.21.0/icon-colors.md index f2f5baa..59bd6bd 100644 --- a/docs/user/versioned_docs/version-v0.21.0/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.21.0/icon-colors.md @@ -14,17 +14,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -38,12 +38,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Price trend sensors (e.g., `price_trend_next_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Price trend sensors (e.g., `price_trend_next_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -51,15 +51,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -295,12 +295,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -426,22 +426,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.21.0/intro.md b/docs/user/versioned_docs/version-v0.21.0/intro.md index 3546e94..6775d0d 100644 --- a/docs/user/versioned_docs/version-v0.21.0/intro.md +++ b/docs/user/versioned_docs/version-v0.21.0/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Multi-currency support** - EUR, NOK, SEK with proper minor units (ct, øre, öre) +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Multi-currency support** - EUR, NOK, SEK with proper minor units (ct, øre, öre) ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.21.0/period-calculation.md b/docs/user/versioned_docs/version-v0.21.0/period-calculation.md index 8b86fc7..0e71459 100644 --- a/docs/user/versioned_docs/version-v0.21.0/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.21.0/period-calculation.md @@ -4,16 +4,16 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) --- @@ -23,8 +23,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -42,16 +42,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -131,7 +134,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -145,9 +148,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -187,8 +191,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -205,8 +209,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -221,8 +225,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -265,9 +269,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -301,7 +305,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -311,7 +315,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -350,11 +354,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -366,6 +372,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -399,10 +406,10 @@ Flex 18% + Level=any → SUCCESS! Found 2 periods ✓ ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -435,9 +442,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -464,24 +471,26 @@ automation: **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -490,20 +499,22 @@ automation: **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -513,19 +524,19 @@ automation: # Entity: binary_sensor.tibber_home_best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_avg: 18.5 # Average price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_avg: 18.5 # Average price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated ``` ### Midnight Price Classification Changes @@ -537,12 +548,14 @@ period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -568,6 +581,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -578,8 +592,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor.tibber_home_volatility_today: 8.2% # Low volatility -sensor.tibber_home_volatility_tomorrow: 7.9% # Also low +sensor.tibber_home_volatility_today: 8.2% # Low volatility +sensor.tibber_home_volatility_tomorrow: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -645,15 +660,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor.tibber_home_best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -664,8 +680,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -690,11 +706,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.21.0/sensors.md b/docs/user/versioned_docs/version-v0.21.0/sensors.md index 3871113..f10c3f4 100644 --- a/docs/user/versioned_docs/version-v0.21.0/sensors.md +++ b/docs/user/versioned_docs/version-v0.21.0/sensors.md @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -45,19 +45,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -78,24 +78,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.22.0/actions.md b/docs/user/versioned_docs/version-v0.22.0/actions.md index 7107662..d92b7c6 100644 --- a/docs/user/versioned_docs/version-v0.22.0/actions.md +++ b/docs/user/versioned_docs/version-v0.22.0/actions.md @@ -12,13 +12,13 @@ You can still call them from automations, scripts, and dashboards the same way a **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -50,14 +50,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -73,8 +73,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -123,6 +124,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -140,9 +142,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -150,9 +152,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -185,7 +187,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -193,7 +195,7 @@ response_variable: config type: custom:config-template-card entities: - sensor.tibber_home_tomorrow_data - - sensor.tibber_home_chart_metadata # For dynamic Y-axis + - sensor.tibber_home_chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -210,6 +212,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -249,10 +252,10 @@ If you're still using the `sensor.tibber_home_chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.22.0/automation-examples.md b/docs/user/versioned_docs/version-v0.22.0/automation-examples.md index f7a2705..d8300f7 100644 --- a/docs/user/versioned_docs/version-v0.22.0/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.22.0/automation-examples.md @@ -6,10 +6,10 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- @@ -29,31 +29,32 @@ On days with low price variation (< 15% volatility), the difference between "che ```yaml automation: - - alias: "Dishwasher - Best Price (High Volatility Only)" - description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only act if volatility > 15% (meaningful savings) - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Optional: Ensure dishwasher is idle and door closed - - condition: state - entity_id: binary_sensor.dishwasher_door - state: "off" - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher_smart_plug - - service: notify.mobile_app - data: - message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" + - alias: "Dishwasher - Best Price (High Volatility Only)" + description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only act if volatility > 15% (meaningful savings) + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Optional: Ensure dishwasher is idle and door closed + - condition: state + entity_id: binary_sensor.dishwasher_door + state: "off" + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher_smart_plug + - service: notify.mobile_app + data: + message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" ``` **Why this works:** + - On high-volatility days (e.g., 25% span), Best Price periods save 5-10 ct/kWh - On low-volatility days (e.g., 8% span), savings are only 1-2 ct/kWh - User can manually start dishwasher on low-volatility days without automation interference @@ -64,33 +65,34 @@ Instead of relying on relative classification, check if the absolute price is ch ```yaml automation: - - alias: "Water Heater - Cheap Enough" - description: "Heat water when price is below absolute threshold, regardless of period classification" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Absolute threshold: Only run if < 20 ct/kWh - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 20 - # Optional: Check water temperature - - condition: numeric_state - entity_id: sensor.water_heater_temperature - below: 55 # Only heat if below 55°C - action: - - service: switch.turn_on - target: - entity_id: switch.water_heater - - delay: - hours: 2 # Heat for 2 hours - - service: switch.turn_off - target: - entity_id: switch.water_heater + - alias: "Water Heater - Cheap Enough" + description: "Heat water when price is below absolute threshold, regardless of period classification" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Absolute threshold: Only run if < 20 ct/kWh + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 20 + # Optional: Check water temperature + - condition: numeric_state + entity_id: sensor.water_heater_temperature + below: 55 # Only heat if below 55°C + action: + - service: switch.turn_on + target: + entity_id: switch.water_heater + - delay: + hours: 2 # Heat for 2 hours + - service: switch.turn_off + target: + entity_id: switch.water_heater ``` **Why this works:** + - Period classification can flip at midnight on low-volatility days - Absolute threshold (20 ct/kWh) is stable across midnight boundary - User sets their own "cheap enough" price based on local rates @@ -101,40 +103,41 @@ Most robust approach: Check both volatility and absolute price: ```yaml automation: - - alias: "EV Charging - Smart Strategy" - description: "Charge EV using volatility-aware logic" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check battery level - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - # Strategy: High volatility OR cheap enough - - condition: or - conditions: - # Path 1: High volatility day - trust period classification + - alias: "EV Charging - Smart Strategy" + description: "Charge EV using volatility-aware logic" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check battery level - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Path 2: Low volatility but price is genuinely cheap - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 18 - action: - - service: switch.turn_on - target: - entity_id: switch.ev_charger - - service: notify.mobile_app - data: - message: > - EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh - (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) + entity_id: sensor.ev_battery_level + below: 80 + # Strategy: High volatility OR cheap enough + - condition: or + conditions: + # Path 1: High volatility day - trust period classification + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Path 2: Low volatility but price is genuinely cheap + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 18 + action: + - service: switch.turn_on + target: + entity_id: switch.ev_charger + - service: notify.mobile_app + data: + message: > + EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh + (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) ``` **Why this works:** + - On high-volatility days (> 15%): Trust the Best Price classification - On low-volatility days (< 15%): Only charge if price is actually cheap (< 18 ct/kWh) - Handles midnight flips gracefully: Continues charging if price stays cheap @@ -145,51 +148,52 @@ Prevent automations from stopping mid-cycle when a period flips at midnight: ```yaml automation: - - alias: "Washing Machine - Complete Cycle" - description: "Start washing machine during Best Price, ignore midnight flips" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only start if washing machine is idle - - condition: state - entity_id: sensor.washing_machine_state - state: "idle" - # And volatility is meaningful - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - action: - - service: button.press - target: - entity_id: button.washing_machine_eco_program - # Create input_boolean to track active cycle - - service: input_boolean.turn_on - target: - entity_id: input_boolean.washing_machine_auto_started + - alias: "Washing Machine - Complete Cycle" + description: "Start washing machine during Best Price, ignore midnight flips" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only start if washing machine is idle + - condition: state + entity_id: sensor.washing_machine_state + state: "idle" + # And volatility is meaningful + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + action: + - service: button.press + target: + entity_id: button.washing_machine_eco_program + # Create input_boolean to track active cycle + - service: input_boolean.turn_on + target: + entity_id: input_boolean.washing_machine_auto_started - # Separate automation: Clear flag when cycle completes - - alias: "Washing Machine - Cycle Complete" - trigger: - - platform: state - entity_id: sensor.washing_machine_state - to: "finished" - condition: - # Only clear flag if we auto-started it - - condition: state - entity_id: input_boolean.washing_machine_auto_started - state: "on" - action: - - service: input_boolean.turn_off - target: - entity_id: input_boolean.washing_machine_auto_started - - service: notify.mobile_app - data: - message: "Washing cycle complete" + # Separate automation: Clear flag when cycle completes + - alias: "Washing Machine - Cycle Complete" + trigger: + - platform: state + entity_id: sensor.washing_machine_state + to: "finished" + condition: + # Only clear flag if we auto-started it + - condition: state + entity_id: input_boolean.washing_machine_auto_started + state: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.washing_machine_auto_started + - service: notify.mobile_app + data: + message: "Washing cycle complete" ``` **Why this works:** + - Uses `input_boolean` to track auto-started cycles - Won't trigger multiple times if period flips during the 2-3 hour wash cycle - Only triggers on "off" → "on" transitions, not during "on" → "on" continuity @@ -200,25 +204,26 @@ The simplest approach: Use the period's day volatility attribute directly: ```yaml automation: - - alias: "Heat Pump - Smart Heating" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check if the PERIOD'S DAY has meaningful volatility - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} - action: - - service: climate.set_temperature - target: - entity_id: climate.heat_pump - data: - temperature: 22 # Boost temperature during cheap period + - alias: "Heat Pump - Smart Heating" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check if the PERIOD'S DAY has meaningful volatility + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + action: + - service: climate.set_temperature + target: + entity_id: climate.heat_pump + data: + temperature: 22 # Boost temperature during cheap period ``` **Available per-period attributes:** + - `day_volatility_%`: Percentage volatility of the period's day (e.g., 8.2 for 8.2%) - `day_price_min`: Minimum price of the day in minor currency (ct/øre) - `day_price_max`: Maximum price of the day in minor currency (ct/øre) @@ -227,6 +232,7 @@ automation: These attributes are available on both `binary_sensor.tibber_home_best_price_period` and `binary_sensor.tibber_home_peak_price_period`. **Why this works:** + - Each period knows its day's volatility - No need to query separate sensors - Template checks if saving is meaningful (> 15% volatility) @@ -252,9 +258,11 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card ### Prerequisites **Required:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** + - [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -270,8 +278,8 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # or "yesterday", "tomorrow" - level_type: rating_level # or "level" for 5-level view + day: today # or "yesterday", "tomorrow" + level_type: rating_level # or "level" for 5-level view response_variable: apexcharts_config ``` @@ -285,12 +293,13 @@ For a dynamic chart that automatically adapts to data availability: service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: rolling_window # Or omit for same behavior (default) + day: rolling_window # Or omit for same behavior (default) level_type: rating_level response_variable: apexcharts_config ``` **Behavior:** + - **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow - **When tomorrow data not available**: Shows yesterday + today - **Fixed 48h span:** Always shows full 48 hours diff --git a/docs/user/versioned_docs/version-v0.22.0/chart-examples.md b/docs/user/versioned_docs/version-v0.22.0/chart-examples.md index 867620d..0e5c799 100644 --- a/docs/user/versioned_docs/version-v0.22.0/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.22.0/chart-examples.md @@ -8,14 +8,15 @@ This guide showcases the different chart configurations available through the `t The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -29,6 +30,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -43,6 +45,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -59,6 +62,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -73,6 +77,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -80,6 +85,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -93,6 +99,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -107,6 +114,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -114,6 +122,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -155,6 +164,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -166,6 +176,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -191,6 +202,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -205,6 +217,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -217,18 +230,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -243,9 +256,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -260,7 +273,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -292,14 +305,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.22.0/concepts.md b/docs/user/versioned_docs/version-v0.22.0/concepts.md index 2a95bed..1822276 100644 --- a/docs/user/versioned_docs/version-v0.22.0/concepts.md +++ b/docs/user/versioned_docs/version-v0.22.0/concepts.md @@ -21,6 +21,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -33,6 +34,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -53,6 +55,7 @@ This helps you understand if current prices are exceptional or typical. ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -62,6 +65,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.22.0/dashboard-examples.md b/docs/user/versioned_docs/version-v0.22.0/dashboard-examples.md index 0badcb9..795cb74 100644 --- a/docs/user/versioned_docs/version-v0.22.0/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.22.0/dashboard-examples.md @@ -10,13 +10,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor.tibber_home_current_interval_price - name: Current Price - icon: mdi:flash - - entity: sensor.tibber_home_current_interval_rating - name: Price Rating - - entity: sensor.tibber_home_next_interval_price - name: Next Price + - entity: sensor.tibber_home_current_interval_price + name: Current Price + icon: mdi:flash + - entity: sensor.tibber_home_current_interval_rating + name: Price Rating + - entity: sensor.tibber_home_next_interval_price + name: Next Price ``` ## Period Status Cards @@ -26,14 +26,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor.tibber_home_best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor.tibber_home_best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -46,16 +46,16 @@ entity: sensor.tibber_home_current_interval_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -67,19 +67,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor.tibber_home_current_interval_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor.tibber_home_current_interval_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor.tibber_home_best_price_start_time - name: Best Period Starts - - entity: binary_sensor.tibber_home_best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor.tibber_home_best_price_start_time + name: Best Period Starts + - entity: binary_sensor.tibber_home_best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -91,25 +91,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor.tibber_home_current_interval_price - - sensor.tibber_home_current_interval_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor.tibber_home_current_interval_price + - sensor.tibber_home_current_interval_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor.tibber_home_daily_avg_today - - sensor.tibber_home_daily_min_today - - sensor.tibber_home_daily_max_today + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor.tibber_home_daily_avg_today + - sensor.tibber_home_daily_min_today + - sensor.tibber_home_daily_max_today ``` ## Icon Color Integration @@ -119,17 +119,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor.tibber_home_current_interval_price - icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" + - type: entity + entity: sensor.tibber_home_current_interval_price + icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" - - type: entity - entity: binary_sensor.tibber_home_best_price_period - icon_color: green + - type: entity + entity: binary_sensor.tibber_home_best_price_period + icon_color: green - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - icon_color: red + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -142,21 +142,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor.tibber_home_current_interval_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor.tibber_home_current_interval_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor.tibber_home_best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor.tibber_home_best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -166,21 +166,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor.tibber_*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor.tibber_*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.22.0/dynamic-icons.md b/docs/user/versioned_docs/version-v0.22.0/dynamic-icons.md index 7138641..9237b60 100644 --- a/docs/user/versioned_docs/version-v0.22.0/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.22.0/dynamic-icons.md @@ -6,10 +6,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -24,10 +24,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -123,8 +123,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -132,24 +132,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor.tibber_home_best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -157,22 +157,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.22.0/faq.md b/docs/user/versioned_docs/version-v0.22.0/faq.md index 7f50067..3281930 100644 --- a/docs/user/versioned_docs/version-v0.22.0/faq.md +++ b/docs/user/versioned_docs/version-v0.22.0/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -116,18 +126,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -138,19 +148,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.22.0/glossary.md b/docs/user/versioned_docs/version-v0.22.0/glossary.md index 2822918..cc35e91 100644 --- a/docs/user/versioned_docs/version-v0.22.0/glossary.md +++ b/docs/user/versioned_docs/version-v0.22.0/glossary.md @@ -100,6 +100,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.22.0/icon-colors.md b/docs/user/versioned_docs/version-v0.22.0/icon-colors.md index f2f5baa..59bd6bd 100644 --- a/docs/user/versioned_docs/version-v0.22.0/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.22.0/icon-colors.md @@ -14,17 +14,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -38,12 +38,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Price trend sensors (e.g., `price_trend_next_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Price trend sensors (e.g., `price_trend_next_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -51,15 +51,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -295,12 +295,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -426,22 +426,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.22.0/intro.md b/docs/user/versioned_docs/version-v0.22.0/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.22.0/intro.md +++ b/docs/user/versioned_docs/version-v0.22.0/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.22.0/period-calculation.md b/docs/user/versioned_docs/version-v0.22.0/period-calculation.md index fd0fec9..a3e4513 100644 --- a/docs/user/versioned_docs/version-v0.22.0/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.22.0/period-calculation.md @@ -4,16 +4,16 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) --- @@ -23,8 +23,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -42,16 +42,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -131,7 +134,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -145,9 +148,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -187,8 +191,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -205,8 +209,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -221,8 +225,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -265,9 +269,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -301,7 +305,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -311,7 +315,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -350,11 +354,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -366,6 +372,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -399,10 +406,10 @@ Flex 18% + Level=any → SUCCESS! Found 2 periods ✓ ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -435,9 +442,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -464,24 +471,26 @@ automation: **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -490,20 +499,22 @@ automation: **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -513,20 +524,20 @@ automation: # Entity: binary_sensor.tibber_home_best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated ``` ### Midnight Price Classification Changes @@ -538,12 +549,14 @@ period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -569,6 +582,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -579,8 +593,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor.tibber_home_volatility_today: 8.2% # Low volatility -sensor.tibber_home_volatility_tomorrow: 7.9% # Also low +sensor.tibber_home_volatility_today: 8.2% # Low volatility +sensor.tibber_home_volatility_tomorrow: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -646,15 +661,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor.tibber_home_best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -665,8 +681,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -691,11 +707,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.22.0/sensors.md b/docs/user/versioned_docs/version-v0.22.0/sensors.md index 3871113..f10c3f4 100644 --- a/docs/user/versioned_docs/version-v0.22.0/sensors.md +++ b/docs/user/versioned_docs/version-v0.22.0/sensors.md @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -45,19 +45,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -78,24 +78,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.22.1/actions.md b/docs/user/versioned_docs/version-v0.22.1/actions.md index 7107662..d92b7c6 100644 --- a/docs/user/versioned_docs/version-v0.22.1/actions.md +++ b/docs/user/versioned_docs/version-v0.22.1/actions.md @@ -12,13 +12,13 @@ You can still call them from automations, scripts, and dashboards the same way a **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -50,14 +50,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -73,8 +73,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -123,6 +124,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -140,9 +142,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -150,9 +152,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -185,7 +187,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -193,7 +195,7 @@ response_variable: config type: custom:config-template-card entities: - sensor.tibber_home_tomorrow_data - - sensor.tibber_home_chart_metadata # For dynamic Y-axis + - sensor.tibber_home_chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -210,6 +212,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -249,10 +252,10 @@ If you're still using the `sensor.tibber_home_chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.22.1/automation-examples.md b/docs/user/versioned_docs/version-v0.22.1/automation-examples.md index f7a2705..d8300f7 100644 --- a/docs/user/versioned_docs/version-v0.22.1/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.22.1/automation-examples.md @@ -6,10 +6,10 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- @@ -29,31 +29,32 @@ On days with low price variation (< 15% volatility), the difference between "che ```yaml automation: - - alias: "Dishwasher - Best Price (High Volatility Only)" - description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only act if volatility > 15% (meaningful savings) - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Optional: Ensure dishwasher is idle and door closed - - condition: state - entity_id: binary_sensor.dishwasher_door - state: "off" - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher_smart_plug - - service: notify.mobile_app - data: - message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" + - alias: "Dishwasher - Best Price (High Volatility Only)" + description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only act if volatility > 15% (meaningful savings) + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Optional: Ensure dishwasher is idle and door closed + - condition: state + entity_id: binary_sensor.dishwasher_door + state: "off" + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher_smart_plug + - service: notify.mobile_app + data: + message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" ``` **Why this works:** + - On high-volatility days (e.g., 25% span), Best Price periods save 5-10 ct/kWh - On low-volatility days (e.g., 8% span), savings are only 1-2 ct/kWh - User can manually start dishwasher on low-volatility days without automation interference @@ -64,33 +65,34 @@ Instead of relying on relative classification, check if the absolute price is ch ```yaml automation: - - alias: "Water Heater - Cheap Enough" - description: "Heat water when price is below absolute threshold, regardless of period classification" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Absolute threshold: Only run if < 20 ct/kWh - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 20 - # Optional: Check water temperature - - condition: numeric_state - entity_id: sensor.water_heater_temperature - below: 55 # Only heat if below 55°C - action: - - service: switch.turn_on - target: - entity_id: switch.water_heater - - delay: - hours: 2 # Heat for 2 hours - - service: switch.turn_off - target: - entity_id: switch.water_heater + - alias: "Water Heater - Cheap Enough" + description: "Heat water when price is below absolute threshold, regardless of period classification" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Absolute threshold: Only run if < 20 ct/kWh + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 20 + # Optional: Check water temperature + - condition: numeric_state + entity_id: sensor.water_heater_temperature + below: 55 # Only heat if below 55°C + action: + - service: switch.turn_on + target: + entity_id: switch.water_heater + - delay: + hours: 2 # Heat for 2 hours + - service: switch.turn_off + target: + entity_id: switch.water_heater ``` **Why this works:** + - Period classification can flip at midnight on low-volatility days - Absolute threshold (20 ct/kWh) is stable across midnight boundary - User sets their own "cheap enough" price based on local rates @@ -101,40 +103,41 @@ Most robust approach: Check both volatility and absolute price: ```yaml automation: - - alias: "EV Charging - Smart Strategy" - description: "Charge EV using volatility-aware logic" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check battery level - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - # Strategy: High volatility OR cheap enough - - condition: or - conditions: - # Path 1: High volatility day - trust period classification + - alias: "EV Charging - Smart Strategy" + description: "Charge EV using volatility-aware logic" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check battery level - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Path 2: Low volatility but price is genuinely cheap - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 18 - action: - - service: switch.turn_on - target: - entity_id: switch.ev_charger - - service: notify.mobile_app - data: - message: > - EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh - (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) + entity_id: sensor.ev_battery_level + below: 80 + # Strategy: High volatility OR cheap enough + - condition: or + conditions: + # Path 1: High volatility day - trust period classification + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Path 2: Low volatility but price is genuinely cheap + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 18 + action: + - service: switch.turn_on + target: + entity_id: switch.ev_charger + - service: notify.mobile_app + data: + message: > + EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh + (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) ``` **Why this works:** + - On high-volatility days (> 15%): Trust the Best Price classification - On low-volatility days (< 15%): Only charge if price is actually cheap (< 18 ct/kWh) - Handles midnight flips gracefully: Continues charging if price stays cheap @@ -145,51 +148,52 @@ Prevent automations from stopping mid-cycle when a period flips at midnight: ```yaml automation: - - alias: "Washing Machine - Complete Cycle" - description: "Start washing machine during Best Price, ignore midnight flips" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only start if washing machine is idle - - condition: state - entity_id: sensor.washing_machine_state - state: "idle" - # And volatility is meaningful - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - action: - - service: button.press - target: - entity_id: button.washing_machine_eco_program - # Create input_boolean to track active cycle - - service: input_boolean.turn_on - target: - entity_id: input_boolean.washing_machine_auto_started + - alias: "Washing Machine - Complete Cycle" + description: "Start washing machine during Best Price, ignore midnight flips" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only start if washing machine is idle + - condition: state + entity_id: sensor.washing_machine_state + state: "idle" + # And volatility is meaningful + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + action: + - service: button.press + target: + entity_id: button.washing_machine_eco_program + # Create input_boolean to track active cycle + - service: input_boolean.turn_on + target: + entity_id: input_boolean.washing_machine_auto_started - # Separate automation: Clear flag when cycle completes - - alias: "Washing Machine - Cycle Complete" - trigger: - - platform: state - entity_id: sensor.washing_machine_state - to: "finished" - condition: - # Only clear flag if we auto-started it - - condition: state - entity_id: input_boolean.washing_machine_auto_started - state: "on" - action: - - service: input_boolean.turn_off - target: - entity_id: input_boolean.washing_machine_auto_started - - service: notify.mobile_app - data: - message: "Washing cycle complete" + # Separate automation: Clear flag when cycle completes + - alias: "Washing Machine - Cycle Complete" + trigger: + - platform: state + entity_id: sensor.washing_machine_state + to: "finished" + condition: + # Only clear flag if we auto-started it + - condition: state + entity_id: input_boolean.washing_machine_auto_started + state: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.washing_machine_auto_started + - service: notify.mobile_app + data: + message: "Washing cycle complete" ``` **Why this works:** + - Uses `input_boolean` to track auto-started cycles - Won't trigger multiple times if period flips during the 2-3 hour wash cycle - Only triggers on "off" → "on" transitions, not during "on" → "on" continuity @@ -200,25 +204,26 @@ The simplest approach: Use the period's day volatility attribute directly: ```yaml automation: - - alias: "Heat Pump - Smart Heating" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check if the PERIOD'S DAY has meaningful volatility - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} - action: - - service: climate.set_temperature - target: - entity_id: climate.heat_pump - data: - temperature: 22 # Boost temperature during cheap period + - alias: "Heat Pump - Smart Heating" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check if the PERIOD'S DAY has meaningful volatility + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + action: + - service: climate.set_temperature + target: + entity_id: climate.heat_pump + data: + temperature: 22 # Boost temperature during cheap period ``` **Available per-period attributes:** + - `day_volatility_%`: Percentage volatility of the period's day (e.g., 8.2 for 8.2%) - `day_price_min`: Minimum price of the day in minor currency (ct/øre) - `day_price_max`: Maximum price of the day in minor currency (ct/øre) @@ -227,6 +232,7 @@ automation: These attributes are available on both `binary_sensor.tibber_home_best_price_period` and `binary_sensor.tibber_home_peak_price_period`. **Why this works:** + - Each period knows its day's volatility - No need to query separate sensors - Template checks if saving is meaningful (> 15% volatility) @@ -252,9 +258,11 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card ### Prerequisites **Required:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** + - [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -270,8 +278,8 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # or "yesterday", "tomorrow" - level_type: rating_level # or "level" for 5-level view + day: today # or "yesterday", "tomorrow" + level_type: rating_level # or "level" for 5-level view response_variable: apexcharts_config ``` @@ -285,12 +293,13 @@ For a dynamic chart that automatically adapts to data availability: service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: rolling_window # Or omit for same behavior (default) + day: rolling_window # Or omit for same behavior (default) level_type: rating_level response_variable: apexcharts_config ``` **Behavior:** + - **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow - **When tomorrow data not available**: Shows yesterday + today - **Fixed 48h span:** Always shows full 48 hours diff --git a/docs/user/versioned_docs/version-v0.22.1/chart-examples.md b/docs/user/versioned_docs/version-v0.22.1/chart-examples.md index 867620d..0e5c799 100644 --- a/docs/user/versioned_docs/version-v0.22.1/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.22.1/chart-examples.md @@ -8,14 +8,15 @@ This guide showcases the different chart configurations available through the `t The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -29,6 +30,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -43,6 +45,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -59,6 +62,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -73,6 +77,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -80,6 +85,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -93,6 +99,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -107,6 +114,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -114,6 +122,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -155,6 +164,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -166,6 +176,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -191,6 +202,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -205,6 +217,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -217,18 +230,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -243,9 +256,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -260,7 +273,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -292,14 +305,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.22.1/concepts.md b/docs/user/versioned_docs/version-v0.22.1/concepts.md index 2a95bed..1822276 100644 --- a/docs/user/versioned_docs/version-v0.22.1/concepts.md +++ b/docs/user/versioned_docs/version-v0.22.1/concepts.md @@ -21,6 +21,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -33,6 +34,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -53,6 +55,7 @@ This helps you understand if current prices are exceptional or typical. ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -62,6 +65,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.22.1/dashboard-examples.md b/docs/user/versioned_docs/version-v0.22.1/dashboard-examples.md index 0badcb9..795cb74 100644 --- a/docs/user/versioned_docs/version-v0.22.1/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.22.1/dashboard-examples.md @@ -10,13 +10,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor.tibber_home_current_interval_price - name: Current Price - icon: mdi:flash - - entity: sensor.tibber_home_current_interval_rating - name: Price Rating - - entity: sensor.tibber_home_next_interval_price - name: Next Price + - entity: sensor.tibber_home_current_interval_price + name: Current Price + icon: mdi:flash + - entity: sensor.tibber_home_current_interval_rating + name: Price Rating + - entity: sensor.tibber_home_next_interval_price + name: Next Price ``` ## Period Status Cards @@ -26,14 +26,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor.tibber_home_best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor.tibber_home_best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -46,16 +46,16 @@ entity: sensor.tibber_home_current_interval_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -67,19 +67,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor.tibber_home_current_interval_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor.tibber_home_current_interval_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor.tibber_home_best_price_start_time - name: Best Period Starts - - entity: binary_sensor.tibber_home_best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor.tibber_home_best_price_start_time + name: Best Period Starts + - entity: binary_sensor.tibber_home_best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -91,25 +91,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor.tibber_home_current_interval_price - - sensor.tibber_home_current_interval_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor.tibber_home_current_interval_price + - sensor.tibber_home_current_interval_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor.tibber_home_daily_avg_today - - sensor.tibber_home_daily_min_today - - sensor.tibber_home_daily_max_today + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor.tibber_home_daily_avg_today + - sensor.tibber_home_daily_min_today + - sensor.tibber_home_daily_max_today ``` ## Icon Color Integration @@ -119,17 +119,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor.tibber_home_current_interval_price - icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" + - type: entity + entity: sensor.tibber_home_current_interval_price + icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" - - type: entity - entity: binary_sensor.tibber_home_best_price_period - icon_color: green + - type: entity + entity: binary_sensor.tibber_home_best_price_period + icon_color: green - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - icon_color: red + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -142,21 +142,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor.tibber_home_current_interval_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor.tibber_home_current_interval_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor.tibber_home_best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor.tibber_home_best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -166,21 +166,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor.tibber_*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor.tibber_*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.22.1/dynamic-icons.md b/docs/user/versioned_docs/version-v0.22.1/dynamic-icons.md index 7138641..9237b60 100644 --- a/docs/user/versioned_docs/version-v0.22.1/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.22.1/dynamic-icons.md @@ -6,10 +6,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -24,10 +24,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -123,8 +123,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -132,24 +132,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor.tibber_home_best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -157,22 +157,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.22.1/faq.md b/docs/user/versioned_docs/version-v0.22.1/faq.md index 7f50067..3281930 100644 --- a/docs/user/versioned_docs/version-v0.22.1/faq.md +++ b/docs/user/versioned_docs/version-v0.22.1/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -116,18 +126,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -138,19 +148,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.22.1/glossary.md b/docs/user/versioned_docs/version-v0.22.1/glossary.md index 2822918..cc35e91 100644 --- a/docs/user/versioned_docs/version-v0.22.1/glossary.md +++ b/docs/user/versioned_docs/version-v0.22.1/glossary.md @@ -100,6 +100,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.22.1/icon-colors.md b/docs/user/versioned_docs/version-v0.22.1/icon-colors.md index f2f5baa..59bd6bd 100644 --- a/docs/user/versioned_docs/version-v0.22.1/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.22.1/icon-colors.md @@ -14,17 +14,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -38,12 +38,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Price trend sensors (e.g., `price_trend_next_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Price trend sensors (e.g., `price_trend_next_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -51,15 +51,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -295,12 +295,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -426,22 +426,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.22.1/intro.md b/docs/user/versioned_docs/version-v0.22.1/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.22.1/intro.md +++ b/docs/user/versioned_docs/version-v0.22.1/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.22.1/period-calculation.md b/docs/user/versioned_docs/version-v0.22.1/period-calculation.md index fd0fec9..a3e4513 100644 --- a/docs/user/versioned_docs/version-v0.22.1/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.22.1/period-calculation.md @@ -4,16 +4,16 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) --- @@ -23,8 +23,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -42,16 +42,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -131,7 +134,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -145,9 +148,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -187,8 +191,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -205,8 +209,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -221,8 +225,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -265,9 +269,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -301,7 +305,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -311,7 +315,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -350,11 +354,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -366,6 +372,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -399,10 +406,10 @@ Flex 18% + Level=any → SUCCESS! Found 2 periods ✓ ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -435,9 +442,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -464,24 +471,26 @@ automation: **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -490,20 +499,22 @@ automation: **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -513,20 +524,20 @@ automation: # Entity: binary_sensor.tibber_home_best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated ``` ### Midnight Price Classification Changes @@ -538,12 +549,14 @@ period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -569,6 +582,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -579,8 +593,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor.tibber_home_volatility_today: 8.2% # Low volatility -sensor.tibber_home_volatility_tomorrow: 7.9% # Also low +sensor.tibber_home_volatility_today: 8.2% # Low volatility +sensor.tibber_home_volatility_tomorrow: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -646,15 +661,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor.tibber_home_best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -665,8 +681,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -691,11 +707,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.22.1/sensors.md b/docs/user/versioned_docs/version-v0.22.1/sensors.md index 3871113..f10c3f4 100644 --- a/docs/user/versioned_docs/version-v0.22.1/sensors.md +++ b/docs/user/versioned_docs/version-v0.22.1/sensors.md @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -45,19 +45,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -78,24 +78,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.23.0/actions.md b/docs/user/versioned_docs/version-v0.23.0/actions.md index 7107662..d92b7c6 100644 --- a/docs/user/versioned_docs/version-v0.23.0/actions.md +++ b/docs/user/versioned_docs/version-v0.23.0/actions.md @@ -12,13 +12,13 @@ You can still call them from automations, scripts, and dashboards the same way a **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -50,14 +50,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -73,8 +73,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -123,6 +124,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -140,9 +142,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -150,9 +152,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -185,7 +187,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -193,7 +195,7 @@ response_variable: config type: custom:config-template-card entities: - sensor.tibber_home_tomorrow_data - - sensor.tibber_home_chart_metadata # For dynamic Y-axis + - sensor.tibber_home_chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -210,6 +212,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -249,10 +252,10 @@ If you're still using the `sensor.tibber_home_chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.23.0/automation-examples.md b/docs/user/versioned_docs/version-v0.23.0/automation-examples.md index f7a2705..d8300f7 100644 --- a/docs/user/versioned_docs/version-v0.23.0/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.23.0/automation-examples.md @@ -6,10 +6,10 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- @@ -29,31 +29,32 @@ On days with low price variation (< 15% volatility), the difference between "che ```yaml automation: - - alias: "Dishwasher - Best Price (High Volatility Only)" - description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only act if volatility > 15% (meaningful savings) - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Optional: Ensure dishwasher is idle and door closed - - condition: state - entity_id: binary_sensor.dishwasher_door - state: "off" - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher_smart_plug - - service: notify.mobile_app - data: - message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" + - alias: "Dishwasher - Best Price (High Volatility Only)" + description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only act if volatility > 15% (meaningful savings) + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Optional: Ensure dishwasher is idle and door closed + - condition: state + entity_id: binary_sensor.dishwasher_door + state: "off" + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher_smart_plug + - service: notify.mobile_app + data: + message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" ``` **Why this works:** + - On high-volatility days (e.g., 25% span), Best Price periods save 5-10 ct/kWh - On low-volatility days (e.g., 8% span), savings are only 1-2 ct/kWh - User can manually start dishwasher on low-volatility days without automation interference @@ -64,33 +65,34 @@ Instead of relying on relative classification, check if the absolute price is ch ```yaml automation: - - alias: "Water Heater - Cheap Enough" - description: "Heat water when price is below absolute threshold, regardless of period classification" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Absolute threshold: Only run if < 20 ct/kWh - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 20 - # Optional: Check water temperature - - condition: numeric_state - entity_id: sensor.water_heater_temperature - below: 55 # Only heat if below 55°C - action: - - service: switch.turn_on - target: - entity_id: switch.water_heater - - delay: - hours: 2 # Heat for 2 hours - - service: switch.turn_off - target: - entity_id: switch.water_heater + - alias: "Water Heater - Cheap Enough" + description: "Heat water when price is below absolute threshold, regardless of period classification" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Absolute threshold: Only run if < 20 ct/kWh + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 20 + # Optional: Check water temperature + - condition: numeric_state + entity_id: sensor.water_heater_temperature + below: 55 # Only heat if below 55°C + action: + - service: switch.turn_on + target: + entity_id: switch.water_heater + - delay: + hours: 2 # Heat for 2 hours + - service: switch.turn_off + target: + entity_id: switch.water_heater ``` **Why this works:** + - Period classification can flip at midnight on low-volatility days - Absolute threshold (20 ct/kWh) is stable across midnight boundary - User sets their own "cheap enough" price based on local rates @@ -101,40 +103,41 @@ Most robust approach: Check both volatility and absolute price: ```yaml automation: - - alias: "EV Charging - Smart Strategy" - description: "Charge EV using volatility-aware logic" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check battery level - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - # Strategy: High volatility OR cheap enough - - condition: or - conditions: - # Path 1: High volatility day - trust period classification + - alias: "EV Charging - Smart Strategy" + description: "Charge EV using volatility-aware logic" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check battery level - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Path 2: Low volatility but price is genuinely cheap - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 18 - action: - - service: switch.turn_on - target: - entity_id: switch.ev_charger - - service: notify.mobile_app - data: - message: > - EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh - (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) + entity_id: sensor.ev_battery_level + below: 80 + # Strategy: High volatility OR cheap enough + - condition: or + conditions: + # Path 1: High volatility day - trust period classification + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Path 2: Low volatility but price is genuinely cheap + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 18 + action: + - service: switch.turn_on + target: + entity_id: switch.ev_charger + - service: notify.mobile_app + data: + message: > + EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh + (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) ``` **Why this works:** + - On high-volatility days (> 15%): Trust the Best Price classification - On low-volatility days (< 15%): Only charge if price is actually cheap (< 18 ct/kWh) - Handles midnight flips gracefully: Continues charging if price stays cheap @@ -145,51 +148,52 @@ Prevent automations from stopping mid-cycle when a period flips at midnight: ```yaml automation: - - alias: "Washing Machine - Complete Cycle" - description: "Start washing machine during Best Price, ignore midnight flips" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only start if washing machine is idle - - condition: state - entity_id: sensor.washing_machine_state - state: "idle" - # And volatility is meaningful - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - action: - - service: button.press - target: - entity_id: button.washing_machine_eco_program - # Create input_boolean to track active cycle - - service: input_boolean.turn_on - target: - entity_id: input_boolean.washing_machine_auto_started + - alias: "Washing Machine - Complete Cycle" + description: "Start washing machine during Best Price, ignore midnight flips" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only start if washing machine is idle + - condition: state + entity_id: sensor.washing_machine_state + state: "idle" + # And volatility is meaningful + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + action: + - service: button.press + target: + entity_id: button.washing_machine_eco_program + # Create input_boolean to track active cycle + - service: input_boolean.turn_on + target: + entity_id: input_boolean.washing_machine_auto_started - # Separate automation: Clear flag when cycle completes - - alias: "Washing Machine - Cycle Complete" - trigger: - - platform: state - entity_id: sensor.washing_machine_state - to: "finished" - condition: - # Only clear flag if we auto-started it - - condition: state - entity_id: input_boolean.washing_machine_auto_started - state: "on" - action: - - service: input_boolean.turn_off - target: - entity_id: input_boolean.washing_machine_auto_started - - service: notify.mobile_app - data: - message: "Washing cycle complete" + # Separate automation: Clear flag when cycle completes + - alias: "Washing Machine - Cycle Complete" + trigger: + - platform: state + entity_id: sensor.washing_machine_state + to: "finished" + condition: + # Only clear flag if we auto-started it + - condition: state + entity_id: input_boolean.washing_machine_auto_started + state: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.washing_machine_auto_started + - service: notify.mobile_app + data: + message: "Washing cycle complete" ``` **Why this works:** + - Uses `input_boolean` to track auto-started cycles - Won't trigger multiple times if period flips during the 2-3 hour wash cycle - Only triggers on "off" → "on" transitions, not during "on" → "on" continuity @@ -200,25 +204,26 @@ The simplest approach: Use the period's day volatility attribute directly: ```yaml automation: - - alias: "Heat Pump - Smart Heating" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check if the PERIOD'S DAY has meaningful volatility - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} - action: - - service: climate.set_temperature - target: - entity_id: climate.heat_pump - data: - temperature: 22 # Boost temperature during cheap period + - alias: "Heat Pump - Smart Heating" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check if the PERIOD'S DAY has meaningful volatility + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + action: + - service: climate.set_temperature + target: + entity_id: climate.heat_pump + data: + temperature: 22 # Boost temperature during cheap period ``` **Available per-period attributes:** + - `day_volatility_%`: Percentage volatility of the period's day (e.g., 8.2 for 8.2%) - `day_price_min`: Minimum price of the day in minor currency (ct/øre) - `day_price_max`: Maximum price of the day in minor currency (ct/øre) @@ -227,6 +232,7 @@ automation: These attributes are available on both `binary_sensor.tibber_home_best_price_period` and `binary_sensor.tibber_home_peak_price_period`. **Why this works:** + - Each period knows its day's volatility - No need to query separate sensors - Template checks if saving is meaningful (> 15% volatility) @@ -252,9 +258,11 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card ### Prerequisites **Required:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** + - [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -270,8 +278,8 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # or "yesterday", "tomorrow" - level_type: rating_level # or "level" for 5-level view + day: today # or "yesterday", "tomorrow" + level_type: rating_level # or "level" for 5-level view response_variable: apexcharts_config ``` @@ -285,12 +293,13 @@ For a dynamic chart that automatically adapts to data availability: service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: rolling_window # Or omit for same behavior (default) + day: rolling_window # Or omit for same behavior (default) level_type: rating_level response_variable: apexcharts_config ``` **Behavior:** + - **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow - **When tomorrow data not available**: Shows yesterday + today - **Fixed 48h span:** Always shows full 48 hours diff --git a/docs/user/versioned_docs/version-v0.23.0/chart-examples.md b/docs/user/versioned_docs/version-v0.23.0/chart-examples.md index 867620d..0e5c799 100644 --- a/docs/user/versioned_docs/version-v0.23.0/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.23.0/chart-examples.md @@ -8,14 +8,15 @@ This guide showcases the different chart configurations available through the `t The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -29,6 +30,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -43,6 +45,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -59,6 +62,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -73,6 +77,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -80,6 +85,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -93,6 +99,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -107,6 +114,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -114,6 +122,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -155,6 +164,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -166,6 +176,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -191,6 +202,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -205,6 +217,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -217,18 +230,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -243,9 +256,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -260,7 +273,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -292,14 +305,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.23.0/concepts.md b/docs/user/versioned_docs/version-v0.23.0/concepts.md index 2a95bed..1822276 100644 --- a/docs/user/versioned_docs/version-v0.23.0/concepts.md +++ b/docs/user/versioned_docs/version-v0.23.0/concepts.md @@ -21,6 +21,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -33,6 +34,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -53,6 +55,7 @@ This helps you understand if current prices are exceptional or typical. ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -62,6 +65,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.23.0/configuration.md b/docs/user/versioned_docs/version-v0.23.0/configuration.md index e35ffae..9c9242e 100644 --- a/docs/user/versioned_docs/version-v0.23.0/configuration.md +++ b/docs/user/versioned_docs/version-v0.23.0/configuration.md @@ -17,12 +17,14 @@ The integration allows you to choose how average price sensors display their val #### Display Modes **Median (Default):** + - Shows the "middle value" when all prices are sorted - **Resistant to extreme spikes** - one expensive hour doesn't skew the result - Best for understanding **typical price levels** - Example: "What was the typical price today?" **Arithmetic Mean:** + - Shows the mathematical average of all prices - **Includes effect of spikes** - reflects actual cost if consuming evenly - Best for **cost calculations and budgeting** @@ -31,6 +33,7 @@ The integration allows you to choose how average price sensors display their val #### Why This Matters Consider a day with these hourly prices: + ``` 10, 12, 13, 15, 80 ct/kWh ``` @@ -51,6 +54,7 @@ The median tells you the price was **typically** around 13 ct/kWh (4 out of 5 ho ``` This means: + - ✅ You can change the display anytime without breaking automations - ✅ Automations can use both values for different purposes - ✅ No need to create template sensors for the "other" value @@ -58,6 +62,7 @@ This means: #### Affected Sensors This setting applies to: + - Daily average sensors (today, tomorrow) - 24-hour rolling averages (trailing, leading) - Hourly smoothed prices (current hour, next hour) @@ -68,12 +73,14 @@ See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examp #### Choosing Your Display **Choose Median if:** + - 👥 You show prices to users ("What's today like?") - 📊 You want dashboard values that represent typical conditions - 🎯 You compare price levels across days - 🔍 You analyze volatility (comparing typical vs extremes) **Choose Mean if:** + - 💰 You calculate costs and budgets - 📈 You forecast energy expenses - 🧮 You need mathematical accuracy for financial planning diff --git a/docs/user/versioned_docs/version-v0.23.0/dashboard-examples.md b/docs/user/versioned_docs/version-v0.23.0/dashboard-examples.md index 0badcb9..795cb74 100644 --- a/docs/user/versioned_docs/version-v0.23.0/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.23.0/dashboard-examples.md @@ -10,13 +10,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor.tibber_home_current_interval_price - name: Current Price - icon: mdi:flash - - entity: sensor.tibber_home_current_interval_rating - name: Price Rating - - entity: sensor.tibber_home_next_interval_price - name: Next Price + - entity: sensor.tibber_home_current_interval_price + name: Current Price + icon: mdi:flash + - entity: sensor.tibber_home_current_interval_rating + name: Price Rating + - entity: sensor.tibber_home_next_interval_price + name: Next Price ``` ## Period Status Cards @@ -26,14 +26,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor.tibber_home_best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor.tibber_home_best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -46,16 +46,16 @@ entity: sensor.tibber_home_current_interval_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -67,19 +67,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor.tibber_home_current_interval_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor.tibber_home_current_interval_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor.tibber_home_best_price_start_time - name: Best Period Starts - - entity: binary_sensor.tibber_home_best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor.tibber_home_best_price_start_time + name: Best Period Starts + - entity: binary_sensor.tibber_home_best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -91,25 +91,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor.tibber_home_current_interval_price - - sensor.tibber_home_current_interval_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor.tibber_home_current_interval_price + - sensor.tibber_home_current_interval_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor.tibber_home_daily_avg_today - - sensor.tibber_home_daily_min_today - - sensor.tibber_home_daily_max_today + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor.tibber_home_daily_avg_today + - sensor.tibber_home_daily_min_today + - sensor.tibber_home_daily_max_today ``` ## Icon Color Integration @@ -119,17 +119,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor.tibber_home_current_interval_price - icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" + - type: entity + entity: sensor.tibber_home_current_interval_price + icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" - - type: entity - entity: binary_sensor.tibber_home_best_price_period - icon_color: green + - type: entity + entity: binary_sensor.tibber_home_best_price_period + icon_color: green - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - icon_color: red + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -142,21 +142,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor.tibber_home_current_interval_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor.tibber_home_current_interval_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor.tibber_home_best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor.tibber_home_best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -166,21 +166,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor.tibber_*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor.tibber_*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.23.0/dynamic-icons.md b/docs/user/versioned_docs/version-v0.23.0/dynamic-icons.md index 7138641..9237b60 100644 --- a/docs/user/versioned_docs/version-v0.23.0/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.23.0/dynamic-icons.md @@ -6,10 +6,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -24,10 +24,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -123,8 +123,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -132,24 +132,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor.tibber_home_best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -157,22 +157,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.23.0/faq.md b/docs/user/versioned_docs/version-v0.23.0/faq.md index 7f50067..3281930 100644 --- a/docs/user/versioned_docs/version-v0.23.0/faq.md +++ b/docs/user/versioned_docs/version-v0.23.0/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -116,18 +126,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -138,19 +148,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.23.0/glossary.md b/docs/user/versioned_docs/version-v0.23.0/glossary.md index 2822918..cc35e91 100644 --- a/docs/user/versioned_docs/version-v0.23.0/glossary.md +++ b/docs/user/versioned_docs/version-v0.23.0/glossary.md @@ -100,6 +100,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.23.0/icon-colors.md b/docs/user/versioned_docs/version-v0.23.0/icon-colors.md index f2f5baa..59bd6bd 100644 --- a/docs/user/versioned_docs/version-v0.23.0/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.23.0/icon-colors.md @@ -14,17 +14,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -38,12 +38,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Price trend sensors (e.g., `price_trend_next_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Price trend sensors (e.g., `price_trend_next_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -51,15 +51,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -295,12 +295,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -426,22 +426,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.23.0/intro.md b/docs/user/versioned_docs/version-v0.23.0/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.23.0/intro.md +++ b/docs/user/versioned_docs/version-v0.23.0/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.23.0/period-calculation.md b/docs/user/versioned_docs/version-v0.23.0/period-calculation.md index fd0fec9..a3e4513 100644 --- a/docs/user/versioned_docs/version-v0.23.0/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.23.0/period-calculation.md @@ -4,16 +4,16 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) --- @@ -23,8 +23,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -42,16 +42,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -131,7 +134,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -145,9 +148,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -187,8 +191,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -205,8 +209,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -221,8 +225,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -265,9 +269,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -301,7 +305,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -311,7 +315,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -350,11 +354,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -366,6 +372,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -399,10 +406,10 @@ Flex 18% + Level=any → SUCCESS! Found 2 periods ✓ ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -435,9 +442,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -464,24 +471,26 @@ automation: **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -490,20 +499,22 @@ automation: **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -513,20 +524,20 @@ automation: # Entity: binary_sensor.tibber_home_best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated ``` ### Midnight Price Classification Changes @@ -538,12 +549,14 @@ period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -569,6 +582,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -579,8 +593,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor.tibber_home_volatility_today: 8.2% # Low volatility -sensor.tibber_home_volatility_tomorrow: 7.9% # Also low +sensor.tibber_home_volatility_today: 8.2% # Low volatility +sensor.tibber_home_volatility_tomorrow: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -646,15 +661,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor.tibber_home_best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -665,8 +681,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -691,11 +707,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.23.0/sensors.md b/docs/user/versioned_docs/version-v0.23.0/sensors.md index 40afef9..6d75087 100644 --- a/docs/user/versioned_docs/version-v0.23.0/sensors.md +++ b/docs/user/versioned_docs/version-v0.23.0/sensors.md @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -29,15 +29,15 @@ The integration provides several sensors that calculate average electricity pric #### Available Average Sensors -| Sensor | Description | Time Window | -|--------|-------------|-------------| -| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | -| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | -| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | -| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | -| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | -| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | -| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | +| Sensor | Description | Time Window | +| -------------------------- | -------------------------------------- | ------------------------------- | +| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | +| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | +| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | +| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | +| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | +| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | +| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | #### Configurable Display: Median vs Mean @@ -66,8 +66,8 @@ You can choose which value is displayed in the sensor state: 2. Click **Configure** on your home 3. Navigate to **Step 6: Average Sensor Display Settings** 4. Choose between: - - **Median** (default) - Shows typical price level, resistant to spikes - - **Arithmetic Mean** - Shows actual mathematical average + - **Median** (default) - Shows typical price level, resistant to spikes + - **Arithmetic Mean** - Shows actual mathematical average **Important:** Both values are **always available** as sensor attributes, regardless of your choice! This ensures your automations continue to work if you change the display setting. @@ -78,22 +78,22 @@ Both `price_mean` and `price_median` are always available as attributes: ```yaml # Example: Get both values regardless of display setting sensor: - - platform: template - sensors: - daily_price_analysis: - friendly_name: "Daily Price Analysis" - value_template: > - {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') %} - {% set mean = state_attr('sensor.tibber_home_average_price_today', 'price_mean') %} - {% set current = states('sensor.tibber_home_current_interval_price') | float %} + - platform: template + sensors: + daily_price_analysis: + friendly_name: "Daily Price Analysis" + value_template: > + {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') %} + {% set mean = state_attr('sensor.tibber_home_average_price_today', 'price_mean') %} + {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% if current < median %} - Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) - {% elif current < mean %} - Typical price range - {% else %} - Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) - {% endif %} + {% if current < median %} + Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) + {% elif current < mean %} + Typical price range + {% else %} + Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) + {% endif %} ``` #### Practical Examples @@ -104,21 +104,21 @@ Run dishwasher only when price is significantly below the daily typical level: ```yaml automation: - - alias: "Start Dishwasher When Cheap" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only if current price is at least 20% below typical (median) - - condition: template - value_template: > - {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') | float %} - {{ current < (median * 0.8) }} - action: - - service: switch.turn_on - entity_id: switch.dishwasher + - alias: "Start Dishwasher When Cheap" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only if current price is at least 20% below typical (median) + - condition: template + value_template: > + {% set current = states('sensor.tibber_home_current_interval_price') | float %} + {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') | float %} + {{ current < (median * 0.8) }} + action: + - service: switch.turn_on + entity_id: switch.dishwasher ``` **Example 2: Cost-aware heating control** @@ -127,20 +127,20 @@ Use mean for actual cost calculations: ```yaml automation: - - alias: "Heating Budget Control" - trigger: - - platform: time - at: "06:00:00" - action: - # Calculate expected daily heating cost - - variables: - mean_price: "{{ state_attr('sensor.tibber_home_average_price_today', 'price_mean') | float }}" - heating_kwh_per_day: 15 # Estimated consumption - daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" - - service: notify.mobile_app - data: - title: "Heating Cost Estimate" - message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" + - alias: "Heating Budget Control" + trigger: + - platform: time + at: "06:00:00" + action: + # Calculate expected daily heating cost + - variables: + mean_price: "{{ state_attr('sensor.tibber_home_average_price_today', 'price_mean') | float }}" + heating_kwh_per_day: 15 # Estimated consumption + daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" + - service: notify.mobile_app + data: + title: "Heating Cost Estimate" + message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" ``` **Example 3: Smart charging based on rolling average** @@ -149,60 +149,61 @@ Use trailing average to understand recent price trends: ```yaml automation: - - alias: "EV Charging - Price Trend Based" - trigger: - - platform: state - entity_id: sensor.ev_battery_level - condition: - # Start charging if current price < 90% of recent 24h average - - condition: template - value_template: > - {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% set trailing_avg = state_attr('sensor.tibber_home_trailing_price_average', 'price_median') | float %} - {{ current < (trailing_avg * 0.9) }} - # And battery < 80% - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - action: - - service: switch.turn_on - entity_id: switch.ev_charger + - alias: "EV Charging - Price Trend Based" + trigger: + - platform: state + entity_id: sensor.ev_battery_level + condition: + # Start charging if current price < 90% of recent 24h average + - condition: template + value_template: > + {% set current = states('sensor.tibber_home_current_interval_price') | float %} + {% set trailing_avg = state_attr('sensor.tibber_home_trailing_price_average', 'price_median') | float %} + {{ current < (trailing_avg * 0.9) }} + # And battery < 80% + - condition: numeric_state + entity_id: sensor.ev_battery_level + below: 80 + action: + - service: switch.turn_on + entity_id: switch.ev_charger ``` #### Key Attributes All average sensors provide these attributes: -| Attribute | Description | Example | -|-----------|-------------|---------| -| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | -| `price_median` | Median value (always available) | 22.1 ct/kWh | -| `interval_count` | Number of intervals included | 96 | -| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | +| Attribute | Description | Example | +| ---------------- | ---------------------------------- | ------------------------- | +| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | +| `price_median` | Median value (always available) | 22.1 ct/kWh | +| `interval_count` | Number of intervals included | 96 | +| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | **Note:** The `price_mean` and `price_median` attributes are **always present** regardless of which value you configured for display. This ensures automation compatibility when changing the display setting. #### When to Use Which Value **Use Median for:** + - ✅ Comparing "typical" price levels across days - ✅ Determining if current price is unusually high/low - ✅ User-facing displays ("What was today like?") - ✅ Volatility analysis (comparing typical vs extremes) **Use Mean for:** + - ✅ Cost calculations and budgeting - ✅ Energy cost estimations - ✅ Comparing actual average costs between periods - ✅ Financial planning and forecasting **Both values tell different stories:** + - High median + much higher mean = Expensive spikes occurred - Low median + higher mean = Generally cheap with occasional spikes - Similar median and mean = Stable prices (low volatility) - - ## Statistical Sensors Coming soon... @@ -223,19 +224,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -256,24 +257,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.23.1/actions.md b/docs/user/versioned_docs/version-v0.23.1/actions.md index 7107662..d92b7c6 100644 --- a/docs/user/versioned_docs/version-v0.23.1/actions.md +++ b/docs/user/versioned_docs/version-v0.23.1/actions.md @@ -12,13 +12,13 @@ You can still call them from automations, scripts, and dashboards the same way a **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -50,14 +50,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -73,8 +73,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -123,6 +124,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -140,9 +142,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -150,9 +152,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -185,7 +187,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -193,7 +195,7 @@ response_variable: config type: custom:config-template-card entities: - sensor.tibber_home_tomorrow_data - - sensor.tibber_home_chart_metadata # For dynamic Y-axis + - sensor.tibber_home_chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -210,6 +212,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -249,10 +252,10 @@ If you're still using the `sensor.tibber_home_chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.23.1/automation-examples.md b/docs/user/versioned_docs/version-v0.23.1/automation-examples.md index f7a2705..d8300f7 100644 --- a/docs/user/versioned_docs/version-v0.23.1/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.23.1/automation-examples.md @@ -6,10 +6,10 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- @@ -29,31 +29,32 @@ On days with low price variation (< 15% volatility), the difference between "che ```yaml automation: - - alias: "Dishwasher - Best Price (High Volatility Only)" - description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only act if volatility > 15% (meaningful savings) - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Optional: Ensure dishwasher is idle and door closed - - condition: state - entity_id: binary_sensor.dishwasher_door - state: "off" - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher_smart_plug - - service: notify.mobile_app - data: - message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" + - alias: "Dishwasher - Best Price (High Volatility Only)" + description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only act if volatility > 15% (meaningful savings) + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Optional: Ensure dishwasher is idle and door closed + - condition: state + entity_id: binary_sensor.dishwasher_door + state: "off" + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher_smart_plug + - service: notify.mobile_app + data: + message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" ``` **Why this works:** + - On high-volatility days (e.g., 25% span), Best Price periods save 5-10 ct/kWh - On low-volatility days (e.g., 8% span), savings are only 1-2 ct/kWh - User can manually start dishwasher on low-volatility days without automation interference @@ -64,33 +65,34 @@ Instead of relying on relative classification, check if the absolute price is ch ```yaml automation: - - alias: "Water Heater - Cheap Enough" - description: "Heat water when price is below absolute threshold, regardless of period classification" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Absolute threshold: Only run if < 20 ct/kWh - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 20 - # Optional: Check water temperature - - condition: numeric_state - entity_id: sensor.water_heater_temperature - below: 55 # Only heat if below 55°C - action: - - service: switch.turn_on - target: - entity_id: switch.water_heater - - delay: - hours: 2 # Heat for 2 hours - - service: switch.turn_off - target: - entity_id: switch.water_heater + - alias: "Water Heater - Cheap Enough" + description: "Heat water when price is below absolute threshold, regardless of period classification" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Absolute threshold: Only run if < 20 ct/kWh + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 20 + # Optional: Check water temperature + - condition: numeric_state + entity_id: sensor.water_heater_temperature + below: 55 # Only heat if below 55°C + action: + - service: switch.turn_on + target: + entity_id: switch.water_heater + - delay: + hours: 2 # Heat for 2 hours + - service: switch.turn_off + target: + entity_id: switch.water_heater ``` **Why this works:** + - Period classification can flip at midnight on low-volatility days - Absolute threshold (20 ct/kWh) is stable across midnight boundary - User sets their own "cheap enough" price based on local rates @@ -101,40 +103,41 @@ Most robust approach: Check both volatility and absolute price: ```yaml automation: - - alias: "EV Charging - Smart Strategy" - description: "Charge EV using volatility-aware logic" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check battery level - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - # Strategy: High volatility OR cheap enough - - condition: or - conditions: - # Path 1: High volatility day - trust period classification + - alias: "EV Charging - Smart Strategy" + description: "Charge EV using volatility-aware logic" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check battery level - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Path 2: Low volatility but price is genuinely cheap - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 18 - action: - - service: switch.turn_on - target: - entity_id: switch.ev_charger - - service: notify.mobile_app - data: - message: > - EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh - (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) + entity_id: sensor.ev_battery_level + below: 80 + # Strategy: High volatility OR cheap enough + - condition: or + conditions: + # Path 1: High volatility day - trust period classification + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Path 2: Low volatility but price is genuinely cheap + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 18 + action: + - service: switch.turn_on + target: + entity_id: switch.ev_charger + - service: notify.mobile_app + data: + message: > + EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh + (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) ``` **Why this works:** + - On high-volatility days (> 15%): Trust the Best Price classification - On low-volatility days (< 15%): Only charge if price is actually cheap (< 18 ct/kWh) - Handles midnight flips gracefully: Continues charging if price stays cheap @@ -145,51 +148,52 @@ Prevent automations from stopping mid-cycle when a period flips at midnight: ```yaml automation: - - alias: "Washing Machine - Complete Cycle" - description: "Start washing machine during Best Price, ignore midnight flips" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only start if washing machine is idle - - condition: state - entity_id: sensor.washing_machine_state - state: "idle" - # And volatility is meaningful - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - action: - - service: button.press - target: - entity_id: button.washing_machine_eco_program - # Create input_boolean to track active cycle - - service: input_boolean.turn_on - target: - entity_id: input_boolean.washing_machine_auto_started + - alias: "Washing Machine - Complete Cycle" + description: "Start washing machine during Best Price, ignore midnight flips" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only start if washing machine is idle + - condition: state + entity_id: sensor.washing_machine_state + state: "idle" + # And volatility is meaningful + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + action: + - service: button.press + target: + entity_id: button.washing_machine_eco_program + # Create input_boolean to track active cycle + - service: input_boolean.turn_on + target: + entity_id: input_boolean.washing_machine_auto_started - # Separate automation: Clear flag when cycle completes - - alias: "Washing Machine - Cycle Complete" - trigger: - - platform: state - entity_id: sensor.washing_machine_state - to: "finished" - condition: - # Only clear flag if we auto-started it - - condition: state - entity_id: input_boolean.washing_machine_auto_started - state: "on" - action: - - service: input_boolean.turn_off - target: - entity_id: input_boolean.washing_machine_auto_started - - service: notify.mobile_app - data: - message: "Washing cycle complete" + # Separate automation: Clear flag when cycle completes + - alias: "Washing Machine - Cycle Complete" + trigger: + - platform: state + entity_id: sensor.washing_machine_state + to: "finished" + condition: + # Only clear flag if we auto-started it + - condition: state + entity_id: input_boolean.washing_machine_auto_started + state: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.washing_machine_auto_started + - service: notify.mobile_app + data: + message: "Washing cycle complete" ``` **Why this works:** + - Uses `input_boolean` to track auto-started cycles - Won't trigger multiple times if period flips during the 2-3 hour wash cycle - Only triggers on "off" → "on" transitions, not during "on" → "on" continuity @@ -200,25 +204,26 @@ The simplest approach: Use the period's day volatility attribute directly: ```yaml automation: - - alias: "Heat Pump - Smart Heating" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check if the PERIOD'S DAY has meaningful volatility - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} - action: - - service: climate.set_temperature - target: - entity_id: climate.heat_pump - data: - temperature: 22 # Boost temperature during cheap period + - alias: "Heat Pump - Smart Heating" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check if the PERIOD'S DAY has meaningful volatility + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + action: + - service: climate.set_temperature + target: + entity_id: climate.heat_pump + data: + temperature: 22 # Boost temperature during cheap period ``` **Available per-period attributes:** + - `day_volatility_%`: Percentage volatility of the period's day (e.g., 8.2 for 8.2%) - `day_price_min`: Minimum price of the day in minor currency (ct/øre) - `day_price_max`: Maximum price of the day in minor currency (ct/øre) @@ -227,6 +232,7 @@ automation: These attributes are available on both `binary_sensor.tibber_home_best_price_period` and `binary_sensor.tibber_home_peak_price_period`. **Why this works:** + - Each period knows its day's volatility - No need to query separate sensors - Template checks if saving is meaningful (> 15% volatility) @@ -252,9 +258,11 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card ### Prerequisites **Required:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** + - [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -270,8 +278,8 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # or "yesterday", "tomorrow" - level_type: rating_level # or "level" for 5-level view + day: today # or "yesterday", "tomorrow" + level_type: rating_level # or "level" for 5-level view response_variable: apexcharts_config ``` @@ -285,12 +293,13 @@ For a dynamic chart that automatically adapts to data availability: service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: rolling_window # Or omit for same behavior (default) + day: rolling_window # Or omit for same behavior (default) level_type: rating_level response_variable: apexcharts_config ``` **Behavior:** + - **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow - **When tomorrow data not available**: Shows yesterday + today - **Fixed 48h span:** Always shows full 48 hours diff --git a/docs/user/versioned_docs/version-v0.23.1/chart-examples.md b/docs/user/versioned_docs/version-v0.23.1/chart-examples.md index 867620d..0e5c799 100644 --- a/docs/user/versioned_docs/version-v0.23.1/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.23.1/chart-examples.md @@ -8,14 +8,15 @@ This guide showcases the different chart configurations available through the `t The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -29,6 +30,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -43,6 +45,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -59,6 +62,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -73,6 +77,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -80,6 +85,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -93,6 +99,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -107,6 +114,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -114,6 +122,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -155,6 +164,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -166,6 +176,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -191,6 +202,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -205,6 +217,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -217,18 +230,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -243,9 +256,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -260,7 +273,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -292,14 +305,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.23.1/concepts.md b/docs/user/versioned_docs/version-v0.23.1/concepts.md index 2a95bed..1822276 100644 --- a/docs/user/versioned_docs/version-v0.23.1/concepts.md +++ b/docs/user/versioned_docs/version-v0.23.1/concepts.md @@ -21,6 +21,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -33,6 +34,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -53,6 +55,7 @@ This helps you understand if current prices are exceptional or typical. ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -62,6 +65,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.23.1/configuration.md b/docs/user/versioned_docs/version-v0.23.1/configuration.md index 4678742..9a0e95e 100644 --- a/docs/user/versioned_docs/version-v0.23.1/configuration.md +++ b/docs/user/versioned_docs/version-v0.23.1/configuration.md @@ -17,12 +17,14 @@ The integration allows you to choose how average price sensors display their val #### Display Modes **Median (Default):** + - Shows the "middle value" when all prices are sorted - **Resistant to extreme spikes** - one expensive hour doesn't skew the result - Best for understanding **typical price levels** - Example: "What was the typical price today?" **Arithmetic Mean:** + - Shows the mathematical average of all prices - **Includes effect of spikes** - reflects actual cost if consuming evenly - Best for **cost calculations and budgeting** @@ -31,6 +33,7 @@ The integration allows you to choose how average price sensors display their val #### Why This Matters Consider a day with these hourly prices: + ``` 10, 12, 13, 15, 80 ct/kWh ``` @@ -51,6 +54,7 @@ The median tells you the price was **typically** around 13 ct/kWh (4 out of 5 ho ``` This means: + - ✅ You can change the display anytime without breaking automations - ✅ Automations can use both values for different purposes - ✅ No need to create template sensors for the "other" value @@ -58,6 +62,7 @@ This means: #### Affected Sensors This setting applies to: + - Daily average sensors (today, tomorrow) - 24-hour rolling averages (trailing, leading) - Hourly smoothed prices (current hour, next hour) @@ -68,12 +73,14 @@ See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examp #### Choosing Your Display **Choose Median if:** + - 👥 You show prices to users ("What's today like?") - 📊 You want dashboard values that represent typical conditions - 🎯 You compare price levels across days - 🔍 You analyze volatility (comparing typical vs extremes) **Choose Mean if:** + - 💰 You calculate costs and budgets - 📈 You forecast energy expenses - 🧮 You need mathematical accuracy for financial planning diff --git a/docs/user/versioned_docs/version-v0.23.1/dashboard-examples.md b/docs/user/versioned_docs/version-v0.23.1/dashboard-examples.md index 0badcb9..795cb74 100644 --- a/docs/user/versioned_docs/version-v0.23.1/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.23.1/dashboard-examples.md @@ -10,13 +10,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor.tibber_home_current_interval_price - name: Current Price - icon: mdi:flash - - entity: sensor.tibber_home_current_interval_rating - name: Price Rating - - entity: sensor.tibber_home_next_interval_price - name: Next Price + - entity: sensor.tibber_home_current_interval_price + name: Current Price + icon: mdi:flash + - entity: sensor.tibber_home_current_interval_rating + name: Price Rating + - entity: sensor.tibber_home_next_interval_price + name: Next Price ``` ## Period Status Cards @@ -26,14 +26,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor.tibber_home_best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor.tibber_home_best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -46,16 +46,16 @@ entity: sensor.tibber_home_current_interval_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -67,19 +67,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor.tibber_home_current_interval_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor.tibber_home_current_interval_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor.tibber_home_best_price_start_time - name: Best Period Starts - - entity: binary_sensor.tibber_home_best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor.tibber_home_best_price_start_time + name: Best Period Starts + - entity: binary_sensor.tibber_home_best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -91,25 +91,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor.tibber_home_current_interval_price - - sensor.tibber_home_current_interval_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor.tibber_home_current_interval_price + - sensor.tibber_home_current_interval_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor.tibber_home_daily_avg_today - - sensor.tibber_home_daily_min_today - - sensor.tibber_home_daily_max_today + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor.tibber_home_daily_avg_today + - sensor.tibber_home_daily_min_today + - sensor.tibber_home_daily_max_today ``` ## Icon Color Integration @@ -119,17 +119,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor.tibber_home_current_interval_price - icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" + - type: entity + entity: sensor.tibber_home_current_interval_price + icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" - - type: entity - entity: binary_sensor.tibber_home_best_price_period - icon_color: green + - type: entity + entity: binary_sensor.tibber_home_best_price_period + icon_color: green - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - icon_color: red + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -142,21 +142,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor.tibber_home_current_interval_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor.tibber_home_current_interval_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor.tibber_home_best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor.tibber_home_best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -166,21 +166,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor.tibber_*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor.tibber_*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.23.1/dynamic-icons.md b/docs/user/versioned_docs/version-v0.23.1/dynamic-icons.md index 7138641..9237b60 100644 --- a/docs/user/versioned_docs/version-v0.23.1/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.23.1/dynamic-icons.md @@ -6,10 +6,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -24,10 +24,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -123,8 +123,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -132,24 +132,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor.tibber_home_best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -157,22 +157,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.23.1/faq.md b/docs/user/versioned_docs/version-v0.23.1/faq.md index 7f50067..3281930 100644 --- a/docs/user/versioned_docs/version-v0.23.1/faq.md +++ b/docs/user/versioned_docs/version-v0.23.1/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -116,18 +126,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -138,19 +148,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.23.1/glossary.md b/docs/user/versioned_docs/version-v0.23.1/glossary.md index 2822918..cc35e91 100644 --- a/docs/user/versioned_docs/version-v0.23.1/glossary.md +++ b/docs/user/versioned_docs/version-v0.23.1/glossary.md @@ -100,6 +100,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.23.1/icon-colors.md b/docs/user/versioned_docs/version-v0.23.1/icon-colors.md index f2f5baa..59bd6bd 100644 --- a/docs/user/versioned_docs/version-v0.23.1/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.23.1/icon-colors.md @@ -14,17 +14,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -38,12 +38,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Price trend sensors (e.g., `price_trend_next_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Price trend sensors (e.g., `price_trend_next_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -51,15 +51,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -295,12 +295,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -426,22 +426,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.23.1/intro.md b/docs/user/versioned_docs/version-v0.23.1/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.23.1/intro.md +++ b/docs/user/versioned_docs/version-v0.23.1/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.23.1/period-calculation.md b/docs/user/versioned_docs/version-v0.23.1/period-calculation.md index fd0fec9..a3e4513 100644 --- a/docs/user/versioned_docs/version-v0.23.1/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.23.1/period-calculation.md @@ -4,16 +4,16 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) --- @@ -23,8 +23,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -42,16 +42,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -131,7 +134,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -145,9 +148,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -187,8 +191,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -205,8 +209,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -221,8 +225,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -265,9 +269,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -301,7 +305,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -311,7 +315,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -350,11 +354,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -366,6 +372,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -399,10 +406,10 @@ Flex 18% + Level=any → SUCCESS! Found 2 periods ✓ ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -435,9 +442,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -464,24 +471,26 @@ automation: **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -490,20 +499,22 @@ automation: **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -513,20 +524,20 @@ automation: # Entity: binary_sensor.tibber_home_best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated ``` ### Midnight Price Classification Changes @@ -538,12 +549,14 @@ period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -569,6 +582,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -579,8 +593,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor.tibber_home_volatility_today: 8.2% # Low volatility -sensor.tibber_home_volatility_tomorrow: 7.9% # Also low +sensor.tibber_home_volatility_today: 8.2% # Low volatility +sensor.tibber_home_volatility_tomorrow: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -646,15 +661,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor.tibber_home_best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -665,8 +681,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -691,11 +707,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.23.1/sensors.md b/docs/user/versioned_docs/version-v0.23.1/sensors.md index 8143303..3e9bba7 100644 --- a/docs/user/versioned_docs/version-v0.23.1/sensors.md +++ b/docs/user/versioned_docs/version-v0.23.1/sensors.md @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -29,15 +29,15 @@ The integration provides several sensors that calculate average electricity pric #### Available Average Sensors -| Sensor | Description | Time Window | -|--------|-------------|-------------| -| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | -| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | -| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | -| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | -| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | -| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | -| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | +| Sensor | Description | Time Window | +| -------------------------- | -------------------------------------- | ------------------------------- | +| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | +| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | +| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | +| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | +| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | +| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | +| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | #### Configurable Display: Median vs Mean @@ -66,8 +66,8 @@ You can choose which value is displayed in the sensor state: 2. Click **Configure** on your home 3. Navigate to **Step 6: Average Sensor Display Settings** 4. Choose between: - - **Median** (default) - Shows typical price level, resistant to spikes - - **Arithmetic Mean** - Shows actual mathematical average + - **Median** (default) - Shows typical price level, resistant to spikes + - **Arithmetic Mean** - Shows actual mathematical average **Important:** Both values are **always available** as sensor attributes, regardless of your choice! This ensures your automations continue to work if you change the display setting. @@ -78,22 +78,22 @@ Both `price_mean` and `price_median` are always available as attributes: ```yaml # Example: Get both values regardless of display setting sensor: - - platform: template - sensors: - daily_price_analysis: - friendly_name: "Daily Price Analysis" - value_template: > - {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') %} - {% set mean = state_attr('sensor.tibber_home_average_price_today', 'price_mean') %} - {% set current = states('sensor.tibber_home_current_interval_price') | float %} + - platform: template + sensors: + daily_price_analysis: + friendly_name: "Daily Price Analysis" + value_template: > + {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') %} + {% set mean = state_attr('sensor.tibber_home_average_price_today', 'price_mean') %} + {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% if current < median %} - Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) - {% elif current < mean %} - Typical price range - {% else %} - Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) - {% endif %} + {% if current < median %} + Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) + {% elif current < mean %} + Typical price range + {% else %} + Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) + {% endif %} ``` #### Practical Examples @@ -104,21 +104,21 @@ Run dishwasher only when price is significantly below the daily typical level: ```yaml automation: - - alias: "Start Dishwasher When Cheap" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only if current price is at least 20% below typical (median) - - condition: template - value_template: > - {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') | float %} - {{ current < (median * 0.8) }} - action: - - service: switch.turn_on - entity_id: switch.dishwasher + - alias: "Start Dishwasher When Cheap" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only if current price is at least 20% below typical (median) + - condition: template + value_template: > + {% set current = states('sensor.tibber_home_current_interval_price') | float %} + {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') | float %} + {{ current < (median * 0.8) }} + action: + - service: switch.turn_on + entity_id: switch.dishwasher ``` **Example 2: Cost-aware heating control** @@ -127,20 +127,20 @@ Use mean for actual cost calculations: ```yaml automation: - - alias: "Heating Budget Control" - trigger: - - platform: time - at: "06:00:00" - action: - # Calculate expected daily heating cost - - variables: - mean_price: "{{ state_attr('sensor.tibber_home_average_price_today', 'price_mean') | float }}" - heating_kwh_per_day: 15 # Estimated consumption - daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" - - service: notify.mobile_app - data: - title: "Heating Cost Estimate" - message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" + - alias: "Heating Budget Control" + trigger: + - platform: time + at: "06:00:00" + action: + # Calculate expected daily heating cost + - variables: + mean_price: "{{ state_attr('sensor.tibber_home_average_price_today', 'price_mean') | float }}" + heating_kwh_per_day: 15 # Estimated consumption + daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" + - service: notify.mobile_app + data: + title: "Heating Cost Estimate" + message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" ``` **Example 3: Smart charging based on rolling average** @@ -149,60 +149,61 @@ Use trailing average to understand recent price trends: ```yaml automation: - - alias: "EV Charging - Price Trend Based" - trigger: - - platform: state - entity_id: sensor.ev_battery_level - condition: - # Start charging if current price < 90% of recent 24h average - - condition: template - value_template: > - {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% set trailing_avg = state_attr('sensor.tibber_home_trailing_price_average', 'price_median') | float %} - {{ current < (trailing_avg * 0.9) }} - # And battery < 80% - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - action: - - service: switch.turn_on - entity_id: switch.ev_charger + - alias: "EV Charging - Price Trend Based" + trigger: + - platform: state + entity_id: sensor.ev_battery_level + condition: + # Start charging if current price < 90% of recent 24h average + - condition: template + value_template: > + {% set current = states('sensor.tibber_home_current_interval_price') | float %} + {% set trailing_avg = state_attr('sensor.tibber_home_trailing_price_average', 'price_median') | float %} + {{ current < (trailing_avg * 0.9) }} + # And battery < 80% + - condition: numeric_state + entity_id: sensor.ev_battery_level + below: 80 + action: + - service: switch.turn_on + entity_id: switch.ev_charger ``` #### Key Attributes All average sensors provide these attributes: -| Attribute | Description | Example | -|-----------|-------------|---------| -| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | -| `price_median` | Median value (always available) | 22.1 ct/kWh | -| `interval_count` | Number of intervals included | 96 | -| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | +| Attribute | Description | Example | +| ---------------- | ---------------------------------- | ------------------------- | +| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | +| `price_median` | Median value (always available) | 22.1 ct/kWh | +| `interval_count` | Number of intervals included | 96 | +| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | **Note:** The `price_mean` and `price_median` attributes are **always present** regardless of which value you configured for display. This ensures automation compatibility when changing the display setting. #### When to Use Which Value **Use Median for:** + - ✅ Comparing "typical" price levels across days - ✅ Determining if current price is unusually high/low - ✅ User-facing displays ("What was today like?") - ✅ Volatility analysis (comparing typical vs extremes) **Use Mean for:** + - ✅ Cost calculations and budgeting - ✅ Energy cost estimations - ✅ Comparing actual average costs between periods - ✅ Financial planning and forecasting **Both values tell different stories:** + - High median + much higher mean = Expensive spikes occurred - Low median + higher mean = Generally cheap with occasional spikes - Similar median and mean = Stable prices (low volatility) - - ## Statistical Sensors Coming soon... @@ -223,19 +224,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -256,24 +257,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.24.0/actions.md b/docs/user/versioned_docs/version-v0.24.0/actions.md index 7107662..d92b7c6 100644 --- a/docs/user/versioned_docs/version-v0.24.0/actions.md +++ b/docs/user/versioned_docs/version-v0.24.0/actions.md @@ -12,13 +12,13 @@ You can still call them from automations, scripts, and dashboards the same way a **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -50,14 +50,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -73,8 +73,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -123,6 +124,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -140,9 +142,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -150,9 +152,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -185,7 +187,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -193,7 +195,7 @@ response_variable: config type: custom:config-template-card entities: - sensor.tibber_home_tomorrow_data - - sensor.tibber_home_chart_metadata # For dynamic Y-axis + - sensor.tibber_home_chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -210,6 +212,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -249,10 +252,10 @@ If you're still using the `sensor.tibber_home_chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.24.0/automation-examples.md b/docs/user/versioned_docs/version-v0.24.0/automation-examples.md index f7a2705..d8300f7 100644 --- a/docs/user/versioned_docs/version-v0.24.0/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.24.0/automation-examples.md @@ -6,10 +6,10 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- @@ -29,31 +29,32 @@ On days with low price variation (< 15% volatility), the difference between "che ```yaml automation: - - alias: "Dishwasher - Best Price (High Volatility Only)" - description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only act if volatility > 15% (meaningful savings) - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Optional: Ensure dishwasher is idle and door closed - - condition: state - entity_id: binary_sensor.dishwasher_door - state: "off" - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher_smart_plug - - service: notify.mobile_app - data: - message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" + - alias: "Dishwasher - Best Price (High Volatility Only)" + description: "Start dishwasher during Best Price period, but only on days with meaningful price differences" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only act if volatility > 15% (meaningful savings) + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Optional: Ensure dishwasher is idle and door closed + - condition: state + entity_id: binary_sensor.dishwasher_door + state: "off" + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher_smart_plug + - service: notify.mobile_app + data: + message: "Dishwasher started during Best Price period ({{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh, volatility {{ states('sensor.tibber_home_volatility_today') }}%)" ``` **Why this works:** + - On high-volatility days (e.g., 25% span), Best Price periods save 5-10 ct/kWh - On low-volatility days (e.g., 8% span), savings are only 1-2 ct/kWh - User can manually start dishwasher on low-volatility days without automation interference @@ -64,33 +65,34 @@ Instead of relying on relative classification, check if the absolute price is ch ```yaml automation: - - alias: "Water Heater - Cheap Enough" - description: "Heat water when price is below absolute threshold, regardless of period classification" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Absolute threshold: Only run if < 20 ct/kWh - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 20 - # Optional: Check water temperature - - condition: numeric_state - entity_id: sensor.water_heater_temperature - below: 55 # Only heat if below 55°C - action: - - service: switch.turn_on - target: - entity_id: switch.water_heater - - delay: - hours: 2 # Heat for 2 hours - - service: switch.turn_off - target: - entity_id: switch.water_heater + - alias: "Water Heater - Cheap Enough" + description: "Heat water when price is below absolute threshold, regardless of period classification" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Absolute threshold: Only run if < 20 ct/kWh + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 20 + # Optional: Check water temperature + - condition: numeric_state + entity_id: sensor.water_heater_temperature + below: 55 # Only heat if below 55°C + action: + - service: switch.turn_on + target: + entity_id: switch.water_heater + - delay: + hours: 2 # Heat for 2 hours + - service: switch.turn_off + target: + entity_id: switch.water_heater ``` **Why this works:** + - Period classification can flip at midnight on low-volatility days - Absolute threshold (20 ct/kWh) is stable across midnight boundary - User sets their own "cheap enough" price based on local rates @@ -101,40 +103,41 @@ Most robust approach: Check both volatility and absolute price: ```yaml automation: - - alias: "EV Charging - Smart Strategy" - description: "Charge EV using volatility-aware logic" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check battery level - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - # Strategy: High volatility OR cheap enough - - condition: or - conditions: - # Path 1: High volatility day - trust period classification + - alias: "EV Charging - Smart Strategy" + description: "Charge EV using volatility-aware logic" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check battery level - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - # Path 2: Low volatility but price is genuinely cheap - - condition: numeric_state - entity_id: sensor.tibber_home_current_interval_price_ct - below: 18 - action: - - service: switch.turn_on - target: - entity_id: switch.ev_charger - - service: notify.mobile_app - data: - message: > - EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh - (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) + entity_id: sensor.ev_battery_level + below: 80 + # Strategy: High volatility OR cheap enough + - condition: or + conditions: + # Path 1: High volatility day - trust period classification + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + # Path 2: Low volatility but price is genuinely cheap + - condition: numeric_state + entity_id: sensor.tibber_home_current_interval_price_ct + below: 18 + action: + - service: switch.turn_on + target: + entity_id: switch.ev_charger + - service: notify.mobile_app + data: + message: > + EV charging started: {{ states('sensor.tibber_home_current_interval_price_ct') }} ct/kWh + (Volatility: {{ states('sensor.tibber_home_volatility_today') }}%) ``` **Why this works:** + - On high-volatility days (> 15%): Trust the Best Price classification - On low-volatility days (< 15%): Only charge if price is actually cheap (< 18 ct/kWh) - Handles midnight flips gracefully: Continues charging if price stays cheap @@ -145,51 +148,52 @@ Prevent automations from stopping mid-cycle when a period flips at midnight: ```yaml automation: - - alias: "Washing Machine - Complete Cycle" - description: "Start washing machine during Best Price, ignore midnight flips" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only start if washing machine is idle - - condition: state - entity_id: sensor.washing_machine_state - state: "idle" - # And volatility is meaningful - - condition: numeric_state - entity_id: sensor.tibber_home_volatility_today - above: 15 - action: - - service: button.press - target: - entity_id: button.washing_machine_eco_program - # Create input_boolean to track active cycle - - service: input_boolean.turn_on - target: - entity_id: input_boolean.washing_machine_auto_started + - alias: "Washing Machine - Complete Cycle" + description: "Start washing machine during Best Price, ignore midnight flips" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only start if washing machine is idle + - condition: state + entity_id: sensor.washing_machine_state + state: "idle" + # And volatility is meaningful + - condition: numeric_state + entity_id: sensor.tibber_home_volatility_today + above: 15 + action: + - service: button.press + target: + entity_id: button.washing_machine_eco_program + # Create input_boolean to track active cycle + - service: input_boolean.turn_on + target: + entity_id: input_boolean.washing_machine_auto_started - # Separate automation: Clear flag when cycle completes - - alias: "Washing Machine - Cycle Complete" - trigger: - - platform: state - entity_id: sensor.washing_machine_state - to: "finished" - condition: - # Only clear flag if we auto-started it - - condition: state - entity_id: input_boolean.washing_machine_auto_started - state: "on" - action: - - service: input_boolean.turn_off - target: - entity_id: input_boolean.washing_machine_auto_started - - service: notify.mobile_app - data: - message: "Washing cycle complete" + # Separate automation: Clear flag when cycle completes + - alias: "Washing Machine - Cycle Complete" + trigger: + - platform: state + entity_id: sensor.washing_machine_state + to: "finished" + condition: + # Only clear flag if we auto-started it + - condition: state + entity_id: input_boolean.washing_machine_auto_started + state: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.washing_machine_auto_started + - service: notify.mobile_app + data: + message: "Washing cycle complete" ``` **Why this works:** + - Uses `input_boolean` to track auto-started cycles - Won't trigger multiple times if period flips during the 2-3 hour wash cycle - Only triggers on "off" → "on" transitions, not during "on" → "on" continuity @@ -200,25 +204,26 @@ The simplest approach: Use the period's day volatility attribute directly: ```yaml automation: - - alias: "Heat Pump - Smart Heating" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Check if the PERIOD'S DAY has meaningful volatility - - condition: template - value_template: > - {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} - action: - - service: climate.set_temperature - target: - entity_id: climate.heat_pump - data: - temperature: 22 # Boost temperature during cheap period + - alias: "Heat Pump - Smart Heating" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Check if the PERIOD'S DAY has meaningful volatility + - condition: template + value_template: > + {{ state_attr('binary_sensor.tibber_home_best_price_period', 'day_volatility_%') | float(0) > 15 }} + action: + - service: climate.set_temperature + target: + entity_id: climate.heat_pump + data: + temperature: 22 # Boost temperature during cheap period ``` **Available per-period attributes:** + - `day_volatility_%`: Percentage volatility of the period's day (e.g., 8.2 for 8.2%) - `day_price_min`: Minimum price of the day in minor currency (ct/øre) - `day_price_max`: Maximum price of the day in minor currency (ct/øre) @@ -227,6 +232,7 @@ automation: These attributes are available on both `binary_sensor.tibber_home_best_price_period` and `binary_sensor.tibber_home_peak_price_period`. **Why this works:** + - Each period knows its day's volatility - No need to query separate sensors - Template checks if saving is meaningful (> 15% volatility) @@ -252,9 +258,11 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card ### Prerequisites **Required:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** + - [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -270,8 +278,8 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # or "yesterday", "tomorrow" - level_type: rating_level # or "level" for 5-level view + day: today # or "yesterday", "tomorrow" + level_type: rating_level # or "level" for 5-level view response_variable: apexcharts_config ``` @@ -285,12 +293,13 @@ For a dynamic chart that automatically adapts to data availability: service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: rolling_window # Or omit for same behavior (default) + day: rolling_window # Or omit for same behavior (default) level_type: rating_level response_variable: apexcharts_config ``` **Behavior:** + - **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow - **When tomorrow data not available**: Shows yesterday + today - **Fixed 48h span:** Always shows full 48 hours diff --git a/docs/user/versioned_docs/version-v0.24.0/chart-examples.md b/docs/user/versioned_docs/version-v0.24.0/chart-examples.md index 867620d..0e5c799 100644 --- a/docs/user/versioned_docs/version-v0.24.0/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.24.0/chart-examples.md @@ -8,14 +8,15 @@ This guide showcases the different chart configurations available through the `t The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -29,6 +30,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -43,6 +45,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -59,6 +62,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -73,6 +77,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -80,6 +85,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -93,6 +99,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -107,6 +114,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -114,6 +122,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -155,6 +164,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -166,6 +176,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -191,6 +202,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -205,6 +217,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -217,18 +230,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -243,9 +256,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -260,7 +273,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -292,14 +305,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.24.0/concepts.md b/docs/user/versioned_docs/version-v0.24.0/concepts.md index 2a95bed..1822276 100644 --- a/docs/user/versioned_docs/version-v0.24.0/concepts.md +++ b/docs/user/versioned_docs/version-v0.24.0/concepts.md @@ -21,6 +21,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -33,6 +34,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -53,6 +55,7 @@ This helps you understand if current prices are exceptional or typical. ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -62,6 +65,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.24.0/configuration.md b/docs/user/versioned_docs/version-v0.24.0/configuration.md index f208140..3b8edd8 100644 --- a/docs/user/versioned_docs/version-v0.24.0/configuration.md +++ b/docs/user/versioned_docs/version-v0.24.0/configuration.md @@ -17,12 +17,14 @@ The integration allows you to choose how average price sensors display their val #### Display Modes **Median (Default):** + - Shows the "middle value" when all prices are sorted - **Resistant to extreme spikes** - one expensive hour doesn't skew the result - Best for understanding **typical price levels** - Example: "What was the typical price today?" **Arithmetic Mean:** + - Shows the mathematical average of all prices - **Includes effect of spikes** - reflects actual cost if consuming evenly - Best for **cost calculations and budgeting** @@ -31,6 +33,7 @@ The integration allows you to choose how average price sensors display their val #### Why This Matters Consider a day with these hourly prices: + ``` 10, 12, 13, 15, 80 ct/kWh ``` @@ -51,6 +54,7 @@ The median tells you the price was **typically** around 13 ct/kWh (4 out of 5 ho ``` This means: + - ✅ You can change the display anytime without breaking automations - ✅ Automations can use both values for different purposes - ✅ No need to create template sensors for the "other" value @@ -58,6 +62,7 @@ This means: #### Affected Sensors This setting applies to: + - Daily average sensors (today, tomorrow) - 24-hour rolling averages (trailing, leading) - Hourly smoothed prices (current hour, next hour) @@ -68,12 +73,14 @@ See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examp #### Choosing Your Display **Choose Median if:** + - 👥 You show prices to users ("What's today like?") - 📊 You want dashboard values that represent typical conditions - 🎯 You compare price levels across days - 🔍 You analyze volatility (comparing typical vs extremes) **Choose Mean if:** + - 💰 You calculate costs and budgets - 📈 You forecast energy expenses - 🧮 You need mathematical accuracy for financial planning diff --git a/docs/user/versioned_docs/version-v0.24.0/dashboard-examples.md b/docs/user/versioned_docs/version-v0.24.0/dashboard-examples.md index 0badcb9..795cb74 100644 --- a/docs/user/versioned_docs/version-v0.24.0/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.24.0/dashboard-examples.md @@ -10,13 +10,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor.tibber_home_current_interval_price - name: Current Price - icon: mdi:flash - - entity: sensor.tibber_home_current_interval_rating - name: Price Rating - - entity: sensor.tibber_home_next_interval_price - name: Next Price + - entity: sensor.tibber_home_current_interval_price + name: Current Price + icon: mdi:flash + - entity: sensor.tibber_home_current_interval_rating + name: Price Rating + - entity: sensor.tibber_home_next_interval_price + name: Next Price ``` ## Period Status Cards @@ -26,14 +26,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor.tibber_home_best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor.tibber_home_best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -46,16 +46,16 @@ entity: sensor.tibber_home_current_interval_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -67,19 +67,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor.tibber_home_current_interval_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor.tibber_home_current_interval_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor.tibber_home_best_price_start_time - name: Best Period Starts - - entity: binary_sensor.tibber_home_best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor.tibber_home_best_price_start_time + name: Best Period Starts + - entity: binary_sensor.tibber_home_best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -91,25 +91,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor.tibber_home_current_interval_price - - sensor.tibber_home_current_interval_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor.tibber_home_current_interval_price + - sensor.tibber_home_current_interval_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor.tibber_home_daily_avg_today - - sensor.tibber_home_daily_min_today - - sensor.tibber_home_daily_max_today + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor.tibber_home_daily_avg_today + - sensor.tibber_home_daily_min_today + - sensor.tibber_home_daily_max_today ``` ## Icon Color Integration @@ -119,17 +119,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor.tibber_home_current_interval_price - icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" + - type: entity + entity: sensor.tibber_home_current_interval_price + icon_color: "{{ state_attr('sensor.tibber_home_current_interval_price', 'icon_color') }}" - - type: entity - entity: binary_sensor.tibber_home_best_price_period - icon_color: green + - type: entity + entity: binary_sensor.tibber_home_best_price_period + icon_color: green - - type: entity - entity: binary_sensor.tibber_home_peak_price_period - icon_color: red + - type: entity + entity: binary_sensor.tibber_home_peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -142,21 +142,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor.tibber_home_current_interval_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor.tibber_home_current_interval_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor.tibber_home_best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor.tibber_home_best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -166,21 +166,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor.tibber_*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor.tibber_*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.24.0/dynamic-icons.md b/docs/user/versioned_docs/version-v0.24.0/dynamic-icons.md index 7138641..9237b60 100644 --- a/docs/user/versioned_docs/version-v0.24.0/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.24.0/dynamic-icons.md @@ -6,10 +6,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -24,10 +24,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -123,8 +123,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -132,24 +132,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor.tibber_home_best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -157,22 +157,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.24.0/faq.md b/docs/user/versioned_docs/version-v0.24.0/faq.md index 7f50067..3281930 100644 --- a/docs/user/versioned_docs/version-v0.24.0/faq.md +++ b/docs/user/versioned_docs/version-v0.24.0/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -116,18 +126,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -138,19 +148,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.24.0/glossary.md b/docs/user/versioned_docs/version-v0.24.0/glossary.md index 2822918..cc35e91 100644 --- a/docs/user/versioned_docs/version-v0.24.0/glossary.md +++ b/docs/user/versioned_docs/version-v0.24.0/glossary.md @@ -100,6 +100,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.24.0/icon-colors.md b/docs/user/versioned_docs/version-v0.24.0/icon-colors.md index f2f5baa..59bd6bd 100644 --- a/docs/user/versioned_docs/version-v0.24.0/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.24.0/icon-colors.md @@ -14,17 +14,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -38,12 +38,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_interval_price_level`) -- Price rating sensors (e.g., `current_interval_price_rating`) -- Volatility sensors (e.g., `volatility_today`) -- Price trend sensors (e.g., `price_trend_next_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_interval_price_level`) +- Price rating sensors (e.g., `current_interval_price_rating`) +- Volatility sensors (e.g., `volatility_today`) +- Price trend sensors (e.g., `price_trend_next_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -51,15 +51,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -295,12 +295,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -426,22 +426,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.24.0/intro.md b/docs/user/versioned_docs/version-v0.24.0/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.24.0/intro.md +++ b/docs/user/versioned_docs/version-v0.24.0/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.24.0/period-calculation.md b/docs/user/versioned_docs/version-v0.24.0/period-calculation.md index fd0fec9..a3e4513 100644 --- a/docs/user/versioned_docs/version-v0.24.0/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.24.0/period-calculation.md @@ -4,16 +4,16 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) --- @@ -23,8 +23,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -42,16 +42,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -131,7 +134,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -145,9 +148,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -187,8 +191,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -205,8 +209,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -221,8 +225,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -265,9 +269,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -301,7 +305,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -311,7 +315,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -350,11 +354,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -366,6 +372,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -399,10 +406,10 @@ Flex 18% + Level=any → SUCCESS! Found 2 periods ✓ ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -435,9 +442,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -464,24 +471,26 @@ automation: **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -490,20 +499,22 @@ automation: **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -513,20 +524,20 @@ automation: # Entity: binary_sensor.tibber_home_best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated ``` ### Midnight Price Classification Changes @@ -538,12 +549,14 @@ period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -569,6 +582,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -579,8 +593,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor.tibber_home_volatility_today: 8.2% # Low volatility -sensor.tibber_home_volatility_tomorrow: 7.9% # Also low +sensor.tibber_home_volatility_today: 8.2% # Low volatility +sensor.tibber_home_volatility_tomorrow: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -646,15 +661,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor.tibber_home_best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -665,8 +681,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -691,11 +707,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.24.0/sensors.md b/docs/user/versioned_docs/version-v0.24.0/sensors.md index 5f21604..170a452 100644 --- a/docs/user/versioned_docs/version-v0.24.0/sensors.md +++ b/docs/user/versioned_docs/version-v0.24.0/sensors.md @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -29,15 +29,15 @@ The integration provides several sensors that calculate average electricity pric #### Available Average Sensors -| Sensor | Description | Time Window | -|--------|-------------|-------------| -| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | -| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | -| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | -| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | -| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | -| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | -| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | +| Sensor | Description | Time Window | +| -------------------------- | -------------------------------------- | ------------------------------- | +| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | +| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | +| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | +| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | +| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | +| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | +| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | #### Configurable Display: Median vs Mean @@ -66,8 +66,8 @@ You can choose which value is displayed in the sensor state: 2. Click **Configure** on your home 3. Navigate to **Step 6: Average Sensor Display Settings** 4. Choose between: - - **Median** (default) - Shows typical price level, resistant to spikes - - **Arithmetic Mean** - Shows actual mathematical average + - **Median** (default) - Shows typical price level, resistant to spikes + - **Arithmetic Mean** - Shows actual mathematical average **Important:** Both values are **always available** as sensor attributes, regardless of your choice! This ensures your automations continue to work if you change the display setting. @@ -78,22 +78,22 @@ Both `price_mean` and `price_median` are always available as attributes: ```yaml # Example: Get both values regardless of display setting sensor: - - platform: template - sensors: - daily_price_analysis: - friendly_name: "Daily Price Analysis" - value_template: > - {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') %} - {% set mean = state_attr('sensor.tibber_home_average_price_today', 'price_mean') %} - {% set current = states('sensor.tibber_home_current_interval_price') | float %} + - platform: template + sensors: + daily_price_analysis: + friendly_name: "Daily Price Analysis" + value_template: > + {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') %} + {% set mean = state_attr('sensor.tibber_home_average_price_today', 'price_mean') %} + {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% if current < median %} - Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) - {% elif current < mean %} - Typical price range - {% else %} - Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) - {% endif %} + {% if current < median %} + Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) + {% elif current < mean %} + Typical price range + {% else %} + Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) + {% endif %} ``` #### Practical Examples @@ -104,21 +104,21 @@ Run dishwasher only when price is significantly below the daily typical level: ```yaml automation: - - alias: "Start Dishwasher When Cheap" - trigger: - - platform: state - entity_id: binary_sensor.tibber_home_best_price_period - to: "on" - condition: - # Only if current price is at least 20% below typical (median) - - condition: template - value_template: > - {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') | float %} - {{ current < (median * 0.8) }} - action: - - service: switch.turn_on - entity_id: switch.dishwasher + - alias: "Start Dishwasher When Cheap" + trigger: + - platform: state + entity_id: binary_sensor.tibber_home_best_price_period + to: "on" + condition: + # Only if current price is at least 20% below typical (median) + - condition: template + value_template: > + {% set current = states('sensor.tibber_home_current_interval_price') | float %} + {% set median = state_attr('sensor.tibber_home_average_price_today', 'price_median') | float %} + {{ current < (median * 0.8) }} + action: + - service: switch.turn_on + entity_id: switch.dishwasher ``` **Example 2: Cost-aware heating control** @@ -127,20 +127,20 @@ Use mean for actual cost calculations: ```yaml automation: - - alias: "Heating Budget Control" - trigger: - - platform: time - at: "06:00:00" - action: - # Calculate expected daily heating cost - - variables: - mean_price: "{{ state_attr('sensor.tibber_home_average_price_today', 'price_mean') | float }}" - heating_kwh_per_day: 15 # Estimated consumption - daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" - - service: notify.mobile_app - data: - title: "Heating Cost Estimate" - message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" + - alias: "Heating Budget Control" + trigger: + - platform: time + at: "06:00:00" + action: + # Calculate expected daily heating cost + - variables: + mean_price: "{{ state_attr('sensor.tibber_home_average_price_today', 'price_mean') | float }}" + heating_kwh_per_day: 15 # Estimated consumption + daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" + - service: notify.mobile_app + data: + title: "Heating Cost Estimate" + message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" ``` **Example 3: Smart charging based on rolling average** @@ -149,60 +149,61 @@ Use trailing average to understand recent price trends: ```yaml automation: - - alias: "EV Charging - Price Trend Based" - trigger: - - platform: state - entity_id: sensor.ev_battery_level - condition: - # Start charging if current price < 90% of recent 24h average - - condition: template - value_template: > - {% set current = states('sensor.tibber_home_current_interval_price') | float %} - {% set trailing_avg = state_attr('sensor.tibber_home_trailing_price_average', 'price_median') | float %} - {{ current < (trailing_avg * 0.9) }} - # And battery < 80% - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - action: - - service: switch.turn_on - entity_id: switch.ev_charger + - alias: "EV Charging - Price Trend Based" + trigger: + - platform: state + entity_id: sensor.ev_battery_level + condition: + # Start charging if current price < 90% of recent 24h average + - condition: template + value_template: > + {% set current = states('sensor.tibber_home_current_interval_price') | float %} + {% set trailing_avg = state_attr('sensor.tibber_home_trailing_price_average', 'price_median') | float %} + {{ current < (trailing_avg * 0.9) }} + # And battery < 80% + - condition: numeric_state + entity_id: sensor.ev_battery_level + below: 80 + action: + - service: switch.turn_on + entity_id: switch.ev_charger ``` #### Key Attributes All average sensors provide these attributes: -| Attribute | Description | Example | -|-----------|-------------|---------| -| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | -| `price_median` | Median value (always available) | 22.1 ct/kWh | -| `interval_count` | Number of intervals included | 96 | -| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | +| Attribute | Description | Example | +| ---------------- | ---------------------------------- | ------------------------- | +| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | +| `price_median` | Median value (always available) | 22.1 ct/kWh | +| `interval_count` | Number of intervals included | 96 | +| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | **Note:** The `price_mean` and `price_median` attributes are **always present** regardless of which value you configured for display. This ensures automation compatibility when changing the display setting. #### When to Use Which Value **Use Median for:** + - ✅ Comparing "typical" price levels across days - ✅ Determining if current price is unusually high/low - ✅ User-facing displays ("What was today like?") - ✅ Volatility analysis (comparing typical vs extremes) **Use Mean for:** + - ✅ Cost calculations and budgeting - ✅ Energy cost estimations - ✅ Comparing actual average costs between periods - ✅ Financial planning and forecasting **Both values tell different stories:** + - High median + much higher mean = Expensive spikes occurred - Low median + higher mean = Generally cheap with occasional spikes - Similar median and mean = Stable prices (low volatility) - - ## Statistical Sensors Coming soon... @@ -223,19 +224,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -256,24 +257,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.27.0/actions.md b/docs/user/versioned_docs/version-v0.27.0/actions.md index 636879c..f0223a7 100644 --- a/docs/user/versioned_docs/version-v0.27.0/actions.md +++ b/docs/user/versioned_docs/version-v0.27.0/actions.md @@ -14,13 +14,13 @@ You can still call them from automations, scripts, and dashboards the same way a **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -52,14 +52,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -75,8 +75,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -125,6 +126,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -142,9 +144,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -152,9 +154,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -187,7 +189,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -195,7 +197,7 @@ response_variable: config type: custom:config-template-card entities: - binary_sensor._tomorrow_s_data_available - - sensor._chart_metadata # For dynamic Y-axis + - sensor._chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -212,6 +214,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -251,10 +254,10 @@ If you're still using the `sensor._chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.27.0/automation-examples.md b/docs/user/versioned_docs/version-v0.27.0/automation-examples.md index aec1d77..67c8ec1 100644 --- a/docs/user/versioned_docs/version-v0.27.0/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.27.0/automation-examples.md @@ -6,16 +6,17 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- > **Important Note:** The following examples are intended as templates to illustrate the logic. They are **not** suitable for direct copy & paste without adaptation. > > Please make sure you: +> > 1. Replace the **Entity IDs** (e.g., `sensor._...`, `switch.pool_pump`) with the IDs of your own devices and sensors. > 2. Adapt the logic to your specific devices (e.g., heat pump, EV, water boiler). > @@ -66,16 +67,15 @@ automation: - service: notify.mobile_app data: message: > - Home battery charging started. Price: {{ states('sensor._current_electricity_price') }} {{ state_attr('sensor._current_electricity_price', 'unit_of_measurement') }}. - Today's volatility is {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') }}. - + Home battery charging started. Price: {{ states('sensor._current_electricity_price') }} {{ state_attr('sensor._current_electricity_price', 'unit_of_measurement') }}. + Today's volatility is {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') }}. ``` **Why this works:** -- The automation only runs if volatility is `moderate`, `high`, or `very_high`. -- If you adjust your volatility thresholds in the future, this automation adapts automatically without any changes. -- It uses the `price_volatility` attribute, ensuring it works correctly regardless of your Home Assistant's display language. +- The automation only runs if volatility is `moderate`, `high`, or `very_high`. +- If you adjust your volatility thresholds in the future, this automation adapts automatically without any changes. +- It uses the `price_volatility` attribute, ensuring it works correctly regardless of your Home Assistant's display language. ### Use Case: Combined Volatility and Absolute Price Check @@ -118,9 +118,9 @@ automation: **Why this works:** -- On days with meaningful price swings, it charges during any `Best Price` period. -- On days with flat prices, it still charges if the price drops below your personal "cheap enough" threshold (e.g., 0.18 €/kWh or 18 ct/kWh). -- This gracefully handles midnight period flips, as the absolute price check will likely remain true if prices stay low. +- On days with meaningful price swings, it charges during any `Best Price` period. +- On days with flat prices, it still charges if the price drops below your personal "cheap enough" threshold (e.g., 0.18 €/kWh or 18 ct/kWh). +- This gracefully handles midnight period flips, as the absolute price check will likely remain true if prices stay low. ### Use Case: Using the Period's Own Volatility Attribute @@ -149,10 +149,10 @@ automation: **Why this works:** -- Each detected period has its own `volatility` attribute (`low`, `moderate`, etc.). -- This is the simplest way to check for meaningful savings for that specific period. -- The attribute name on the binary sensor is `volatility` (lowercase) and its value is also lowercase. -- It also contains other useful attributes like `price_mean`, `price_spread`, and the `price_coefficient_variation_%` for that period. +- Each detected period has its own `volatility` attribute (`low`, `moderate`, etc.). +- This is the simplest way to check for meaningful savings for that specific period. +- The attribute name on the binary sensor is `volatility` (lowercase) and its value is also lowercase. +- It also contains other useful attributes like `price_mean`, `price_spread`, and the `price_coefficient_variation_%` for that period. --- @@ -176,11 +176,11 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card **Required:** -- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS +- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** -- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS +- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -217,9 +217,9 @@ response_variable: apexcharts_config **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow -- **When tomorrow data not available**: Shows yesterday + today -- **Fixed 48h span:** Always shows full 48 hours +- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow +- **When tomorrow data not available**: Shows yesterday + today +- **Fixed 48h span:** Always shows full 48 hours **Auto-Zoom Variant:** @@ -234,17 +234,17 @@ data: response_variable: apexcharts_config ``` -- Same data loading as rolling window -- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight -- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight +- Same data loading as rolling window +- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight +- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight **Note:** Rolling window modes require Config Template Card to dynamically adjust the time range. ### Features -- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) -- Best price period highlighting (semi-transparent green overlay) -- Automatic NULL insertion for clean gaps -- Translated labels based on your Home Assistant language -- Interactive zoom and pan -- Live marker showing current time +- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) +- Best price period highlighting (semi-transparent green overlay) +- Automatic NULL insertion for clean gaps +- Translated labels based on your Home Assistant language +- Interactive zoom and pan +- Live marker showing current time diff --git a/docs/user/versioned_docs/version-v0.27.0/chart-examples.md b/docs/user/versioned_docs/version-v0.27.0/chart-examples.md index a782c50..abab858 100644 --- a/docs/user/versioned_docs/version-v0.27.0/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.27.0/chart-examples.md @@ -10,14 +10,15 @@ This guide showcases the different chart configurations available through the `t The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -31,6 +32,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -45,6 +47,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -61,6 +64,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -75,6 +79,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -82,6 +87,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -95,6 +101,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -109,6 +116,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -116,6 +124,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -157,6 +166,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -168,6 +178,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -193,6 +204,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -207,6 +219,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -219,18 +232,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -245,9 +258,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -262,7 +275,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -294,14 +307,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.27.0/concepts.md b/docs/user/versioned_docs/version-v0.27.0/concepts.md index 2a95bed..1822276 100644 --- a/docs/user/versioned_docs/version-v0.27.0/concepts.md +++ b/docs/user/versioned_docs/version-v0.27.0/concepts.md @@ -21,6 +21,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -33,6 +34,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -53,6 +55,7 @@ This helps you understand if current prices are exceptional or typical. ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -62,6 +65,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.27.0/configuration.md b/docs/user/versioned_docs/version-v0.27.0/configuration.md index a034a25..3f48278 100644 --- a/docs/user/versioned_docs/version-v0.27.0/configuration.md +++ b/docs/user/versioned_docs/version-v0.27.0/configuration.md @@ -19,12 +19,14 @@ The integration allows you to choose how average price sensors display their val #### Display Modes **Median (Default):** + - Shows the "middle value" when all prices are sorted - **Resistant to extreme spikes** - one expensive hour doesn't skew the result - Best for understanding **typical price levels** - Example: "What was the typical price today?" **Arithmetic Mean:** + - Shows the mathematical average of all prices - **Includes effect of spikes** - reflects actual cost if consuming evenly - Best for **cost calculations and budgeting** @@ -33,6 +35,7 @@ The integration allows you to choose how average price sensors display their val #### Why This Matters Consider a day with these hourly prices: + ``` 10, 12, 13, 15, 80 ct/kWh ``` @@ -53,6 +56,7 @@ The median tells you the price was **typically** around 13 ct/kWh (4 out of 5 ho ``` This means: + - ✅ You can change the display anytime without breaking automations - ✅ Automations can use both values for different purposes - ✅ No need to create template sensors for the "other" value @@ -60,6 +64,7 @@ This means: #### Affected Sensors This setting applies to: + - Daily average sensors (today, tomorrow) - 24-hour rolling averages (trailing, leading) - Hourly smoothed prices (current hour, next hour) @@ -70,12 +75,14 @@ See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examp #### Choosing Your Display **Choose Median if:** + - 👥 You show prices to users ("What's today like?") - 📊 You want dashboard values that represent typical conditions - 🎯 You compare price levels across days - 🔍 You analyze volatility (comparing typical vs extremes) **Choose Mean if:** + - 💰 You calculate costs and budgets - 📈 You forecast energy expenses - 🧮 You need mathematical accuracy for financial planning @@ -93,27 +100,27 @@ When enabled, these entities override the corresponding Options Flow settings: #### Best Price Period Settings -| Entity | Type | Range | Description | -|--------|------|-------|-------------| -| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals | -| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average | -| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold | -| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Entity | Type | Range | Description | +| ------------------------------------- | ------ | ---------- | ------------------------------------------------------ | +| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals | +| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average | +| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | +| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | +| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | +| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold | +| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | #### Peak Price Period Settings -| Entity | Type | Range | Description | -|--------|------|-------|-------------| -| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | -| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | -| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | -| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Entity | Type | Range | Description | +| ------------------------------------- | ------ | ---------- | ------------------------------------------------------ | +| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | +| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | +| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | +| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | +| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | +| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | +| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | ### How Runtime Overrides Work @@ -138,24 +145,25 @@ Each configuration entity includes a detailed description attribute explaining w ```yaml automation: - - alias: "Winter: Stricter Best Price Detection" - trigger: - - platform: time - at: "00:00:00" - condition: - - condition: template - value_template: "{{ now().month in [11, 12, 1, 2] }}" - action: - - service: number.set_value - target: - entity_id: number._best_price_flexibility_override - data: - value: 10 # Stricter than default 15% + - alias: "Winter: Stricter Best Price Detection" + trigger: + - platform: time + at: "00:00:00" + condition: + - condition: template + value_template: "{{ now().month in [11, 12, 1, 2] }}" + action: + - service: number.set_value + target: + entity_id: number._best_price_flexibility_override + data: + value: 10 # Stricter than default 15% ``` ### Recorder Optimization (Optional) These configuration entities are designed to minimize database impact: + - **EntityCategory.CONFIG** - Excluded from Long-Term Statistics - All attributes excluded from history recording - Only state value changes are recorded @@ -166,16 +174,17 @@ However, if you prefer to **completely exclude** these entities from the recorde ```yaml recorder: - exclude: - entity_globs: - # Exclude all Tibber Prices configuration entities - - number.*_best_price_*_override - - number.*_peak_price_*_override - - switch.*_best_price_*_override - - switch.*_peak_price_*_override + exclude: + entity_globs: + # Exclude all Tibber Prices configuration entities + - number.*_best_price_*_override + - number.*_peak_price_*_override + - switch.*_best_price_*_override + - switch.*_peak_price_*_override ``` This is especially useful if: + - You rarely change these settings - You want the smallest possible database footprint - You don't need to see the history graph for these entities diff --git a/docs/user/versioned_docs/version-v0.27.0/dashboard-examples.md b/docs/user/versioned_docs/version-v0.27.0/dashboard-examples.md index 777b059..9fd206b 100644 --- a/docs/user/versioned_docs/version-v0.27.0/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.27.0/dashboard-examples.md @@ -12,13 +12,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor._current_electricity_price - name: Current Price - icon: mdi:flash - - entity: sensor._current_price_rating - name: Price Rating - - entity: sensor._next_electricity_price - name: Next Price + - entity: sensor._current_electricity_price + name: Current Price + icon: mdi:flash + - entity: sensor._current_price_rating + name: Price Rating + - entity: sensor._next_electricity_price + name: Next Price ``` ## Period Status Cards @@ -28,14 +28,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor._best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor._peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor._best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor._peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -48,16 +48,16 @@ entity: sensor._current_price_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -69,19 +69,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor._current_electricity_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor._current_electricity_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor._best_price_start - name: Best Period Starts - - entity: binary_sensor._best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor._best_price_start + name: Best Period Starts + - entity: binary_sensor._best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -93,25 +93,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor._current_electricity_price - - sensor._current_price_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor._current_electricity_price + - sensor._current_price_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor._price_today - - sensor._today_s_lowest_price - - sensor._today_s_highest_price + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor._price_today + - sensor._today_s_lowest_price + - sensor._today_s_highest_price ``` ## Icon Color Integration @@ -121,17 +121,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor._current_electricity_price - icon_color: "{{ state_attr('sensor._current_electricity_price', 'icon_color') }}" + - type: entity + entity: sensor._current_electricity_price + icon_color: "{{ state_attr('sensor._current_electricity_price', 'icon_color') }}" - - type: entity - entity: binary_sensor._best_price_period - icon_color: green + - type: entity + entity: binary_sensor._best_price_period + icon_color: green - - type: entity - entity: binary_sensor._peak_price_period - icon_color: red + - type: entity + entity: binary_sensor._peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -144,21 +144,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor._current_electricity_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor._current_electricity_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor._best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor._best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -168,21 +168,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor._*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor._*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.27.0/dynamic-icons.md b/docs/user/versioned_docs/version-v0.27.0/dynamic-icons.md index ed53f24..3be732d 100644 --- a/docs/user/versioned_docs/version-v0.27.0/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.27.0/dynamic-icons.md @@ -8,10 +8,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -26,10 +26,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_price_level`) -- Price rating sensors (e.g., `current_price_rating`) -- Volatility sensors (e.g., `today_s_price_volatility`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_price_level`) +- Price rating sensors (e.g., `current_price_rating`) +- Volatility sensors (e.g., `today_s_price_volatility`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -125,8 +125,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -134,24 +134,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor._best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -159,22 +159,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.27.0/faq.md b/docs/user/versioned_docs/version-v0.27.0/faq.md index 260b09d..7cf4220 100644 --- a/docs/user/versioned_docs/version-v0.27.0/faq.md +++ b/docs/user/versioned_docs/version-v0.27.0/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -118,18 +128,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor._best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor._best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -140,19 +150,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor._peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor._peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.27.0/glossary.md b/docs/user/versioned_docs/version-v0.27.0/glossary.md index 2822918..cc35e91 100644 --- a/docs/user/versioned_docs/version-v0.27.0/glossary.md +++ b/docs/user/versioned_docs/version-v0.27.0/glossary.md @@ -100,6 +100,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.27.0/icon-colors.md b/docs/user/versioned_docs/version-v0.27.0/icon-colors.md index 8e1a868..0e17a00 100644 --- a/docs/user/versioned_docs/version-v0.27.0/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.27.0/icon-colors.md @@ -16,17 +16,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -40,12 +40,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_price_level`) -- Price rating sensors (e.g., `current_price_rating`) -- Volatility sensors (e.g., `today_s_price_volatility`) -- Price trend sensors (e.g., `price_trend_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_price_level`) +- Price rating sensors (e.g., `current_price_rating`) +- Volatility sensors (e.g., `today_s_price_volatility`) +- Price trend sensors (e.g., `price_trend_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -53,15 +53,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -297,12 +297,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -428,22 +428,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.27.0/intro.md b/docs/user/versioned_docs/version-v0.27.0/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.27.0/intro.md +++ b/docs/user/versioned_docs/version-v0.27.0/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.27.0/period-calculation.md b/docs/user/versioned_docs/version-v0.27.0/period-calculation.md index 3e63824..3c9a5ba 100644 --- a/docs/user/versioned_docs/version-v0.27.0/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.27.0/period-calculation.md @@ -6,16 +6,16 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) --- @@ -25,8 +25,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -44,16 +44,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -133,7 +136,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -147,9 +150,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -189,8 +193,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -207,8 +211,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -223,8 +227,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -267,9 +271,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -303,7 +307,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -313,7 +317,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -352,11 +356,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -368,6 +374,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -401,10 +408,10 @@ Flex 18% + Level=any → SUCCESS! Found 2 periods ✓ ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -437,9 +444,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -466,24 +473,26 @@ automation: **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -492,20 +501,22 @@ automation: **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -515,20 +526,20 @@ automation: # Entity: binary_sensor._best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated ``` ### Midnight Price Classification Changes @@ -540,12 +551,14 @@ period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -571,6 +584,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -581,8 +595,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor._today_s_price_volatility: 8.2% # Low volatility -sensor._tomorrow_s_price_volatility: 7.9% # Also low +sensor._today_s_price_volatility: 8.2% # Low volatility +sensor._tomorrow_s_price_volatility: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -648,15 +663,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor._best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -667,8 +683,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -693,11 +709,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.27.0/sensors.md b/docs/user/versioned_docs/version-v0.27.0/sensors.md index f93fd2a..a8f0a53 100644 --- a/docs/user/versioned_docs/version-v0.27.0/sensors.md +++ b/docs/user/versioned_docs/version-v0.27.0/sensors.md @@ -18,8 +18,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -31,15 +31,15 @@ The integration provides several sensors that calculate average electricity pric #### Available Average Sensors -| Sensor | Description | Time Window | -|--------|-------------|-------------| -| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | -| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | -| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | -| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | -| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | -| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | -| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | +| Sensor | Description | Time Window | +| -------------------------- | -------------------------------------- | ------------------------------- | +| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | +| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | +| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | +| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | +| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | +| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | +| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | #### Configurable Display: Median vs Mean @@ -68,8 +68,8 @@ You can choose which value is displayed in the sensor state: 2. Click **Configure** on your home 3. Navigate to **Step 6: Average Sensor Display Settings** 4. Choose between: - - **Median** (default) - Shows typical price level, resistant to spikes - - **Arithmetic Mean** - Shows actual mathematical average + - **Median** (default) - Shows typical price level, resistant to spikes + - **Arithmetic Mean** - Shows actual mathematical average **Important:** Both values are **always available** as sensor attributes, regardless of your choice! This ensures your automations continue to work if you change the display setting. @@ -80,22 +80,22 @@ Both `price_mean` and `price_median` are always available as attributes: ```yaml # Example: Get both values regardless of display setting sensor: - - platform: template - sensors: - daily_price_analysis: - friendly_name: "Daily Price Analysis" - value_template: > - {% set median = state_attr('sensor._price_today', 'price_median') %} - {% set mean = state_attr('sensor._price_today', 'price_mean') %} - {% set current = states('sensor._current_electricity_price') | float %} + - platform: template + sensors: + daily_price_analysis: + friendly_name: "Daily Price Analysis" + value_template: > + {% set median = state_attr('sensor._price_today', 'price_median') %} + {% set mean = state_attr('sensor._price_today', 'price_mean') %} + {% set current = states('sensor._current_electricity_price') | float %} - {% if current < median %} - Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) - {% elif current < mean %} - Typical price range - {% else %} - Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) - {% endif %} + {% if current < median %} + Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) + {% elif current < mean %} + Typical price range + {% else %} + Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) + {% endif %} ``` #### Practical Examples @@ -106,21 +106,21 @@ Run dishwasher only when price is significantly below the daily typical level: ```yaml automation: - - alias: "Start Dishwasher When Cheap" - trigger: - - platform: state - entity_id: binary_sensor._best_price_period - to: "on" - condition: - # Only if current price is at least 20% below typical (median) - - condition: template - value_template: > - {% set current = states('sensor._current_electricity_price') | float %} - {% set median = state_attr('sensor._price_today', 'price_median') | float %} - {{ current < (median * 0.8) }} - action: - - service: switch.turn_on - entity_id: switch.dishwasher + - alias: "Start Dishwasher When Cheap" + trigger: + - platform: state + entity_id: binary_sensor._best_price_period + to: "on" + condition: + # Only if current price is at least 20% below typical (median) + - condition: template + value_template: > + {% set current = states('sensor._current_electricity_price') | float %} + {% set median = state_attr('sensor._price_today', 'price_median') | float %} + {{ current < (median * 0.8) }} + action: + - service: switch.turn_on + entity_id: switch.dishwasher ``` **Example 2: Cost-aware heating control** @@ -129,20 +129,20 @@ Use mean for actual cost calculations: ```yaml automation: - - alias: "Heating Budget Control" - trigger: - - platform: time - at: "06:00:00" - action: - # Calculate expected daily heating cost - - variables: - mean_price: "{{ state_attr('sensor._price_today', 'price_mean') | float }}" - heating_kwh_per_day: 15 # Estimated consumption - daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" - - service: notify.mobile_app - data: - title: "Heating Cost Estimate" - message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" + - alias: "Heating Budget Control" + trigger: + - platform: time + at: "06:00:00" + action: + # Calculate expected daily heating cost + - variables: + mean_price: "{{ state_attr('sensor._price_today', 'price_mean') | float }}" + heating_kwh_per_day: 15 # Estimated consumption + daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" + - service: notify.mobile_app + data: + title: "Heating Cost Estimate" + message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" ``` **Example 3: Smart charging based on rolling average** @@ -151,60 +151,61 @@ Use trailing average to understand recent price trends: ```yaml automation: - - alias: "EV Charging - Price Trend Based" - trigger: - - platform: state - entity_id: sensor.ev_battery_level - condition: - # Start charging if current price < 90% of recent 24h average - - condition: template - value_template: > - {% set current = states('sensor._current_electricity_price') | float %} - {% set trailing_avg = state_attr('sensor._price_trailing_24h', 'price_median') | float %} - {{ current < (trailing_avg * 0.9) }} - # And battery < 80% - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - action: - - service: switch.turn_on - entity_id: switch.ev_charger + - alias: "EV Charging - Price Trend Based" + trigger: + - platform: state + entity_id: sensor.ev_battery_level + condition: + # Start charging if current price < 90% of recent 24h average + - condition: template + value_template: > + {% set current = states('sensor._current_electricity_price') | float %} + {% set trailing_avg = state_attr('sensor._price_trailing_24h', 'price_median') | float %} + {{ current < (trailing_avg * 0.9) }} + # And battery < 80% + - condition: numeric_state + entity_id: sensor.ev_battery_level + below: 80 + action: + - service: switch.turn_on + entity_id: switch.ev_charger ``` #### Key Attributes All average sensors provide these attributes: -| Attribute | Description | Example | -|-----------|-------------|---------| -| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | -| `price_median` | Median value (always available) | 22.1 ct/kWh | -| `interval_count` | Number of intervals included | 96 | -| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | +| Attribute | Description | Example | +| ---------------- | ---------------------------------- | ------------------------- | +| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | +| `price_median` | Median value (always available) | 22.1 ct/kWh | +| `interval_count` | Number of intervals included | 96 | +| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | **Note:** The `price_mean` and `price_median` attributes are **always present** regardless of which value you configured for display. This ensures automation compatibility when changing the display setting. #### When to Use Which Value **Use Median for:** + - ✅ Comparing "typical" price levels across days - ✅ Determining if current price is unusually high/low - ✅ User-facing displays ("What was today like?") - ✅ Volatility analysis (comparing typical vs extremes) **Use Mean for:** + - ✅ Cost calculations and budgeting - ✅ Energy cost estimations - ✅ Comparing actual average costs between periods - ✅ Financial planning and forecasting **Both values tell different stories:** + - High median + much higher mean = Expensive spikes occurred - Low median + higher mean = Generally cheap with occasional spikes - Similar median and mean = Stable prices (low volatility) - - ## Volatility Sensors Volatility sensors help you understand how much electricity prices fluctuate over a given period. Instead of just looking at the absolute price, they measure the **relative price variation**, which is a great indicator of whether it's a good day for price-based energy optimization. @@ -219,21 +220,23 @@ The sensor's state can be `low`, `moderate`, `high`, or `very_high`, based on co ### Available Volatility Sensors -| Sensor | Description | Time Window | -|---|---|---| -| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | -| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | -| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | -| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | +| Sensor | Description | Time Window | +| ------------------------------------- | ----------------------------------------- | ---------------------- | +| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | +| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | +| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | +| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | ### Configuration You can adjust the CV thresholds that determine the volatility level: + 1. Go to **Settings → Devices & Services → Tibber Prices**. 2. Click **Configure**. 3. Go to the **Price Volatility Thresholds** step. Default thresholds are: + - **Moderate:** 15% - **High:** 30% - **Very High:** 50% @@ -242,14 +245,14 @@ Default thresholds are: All volatility sensors provide these attributes: -| Attribute | Description | Example | -|---|---|---| -| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` | -| `price_spread` | The difference between the highest and lowest price | `12.3` | -| `price_min` | The lowest price in the period | `10.2` | -| `price_max` | The highest price in the period | `22.5` | -| `price_mean` | The arithmetic mean of all prices in the period | `15.1` | -| `interval_count` | Number of price intervals included in the calculation | `96` | +| Attribute | Description | Example | +| ------------------------------- | ----------------------------------------------------- | ------- | +| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` | +| `price_spread` | The difference between the highest and lowest price | `12.3` | +| `price_min` | The lowest price in the period | `10.2` | +| `price_max` | The highest price in the period | `22.5` | +| `price_mean` | The arithmetic mean of all prices in the period | `15.1` | +| `interval_count` | Number of price intervals included in the calculation | `96` | ### Usage in Automations & Best Practices @@ -263,16 +266,17 @@ For automations, it is strongly recommended to use the `price_volatility` attrib **Good Example (Robust Automation):** This automation triggers only if the volatility is classified as `high` or `very_high`, respecting your central settings and working independently of the system language. + ```yaml automation: - - alias: "Enable battery optimization only on volatile days" - trigger: - - platform: template - value_template: > - {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }} - action: - - service: input_boolean.turn_on - entity_id: input_boolean.battery_optimization_enabled + - alias: "Enable battery optimization only on volatile days" + trigger: + - platform: template + value_template: > + {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }} + action: + - service: input_boolean.turn_on + entity_id: input_boolean.battery_optimization_enabled ``` --- @@ -285,20 +289,21 @@ You might be tempted to use the numeric `price_coefficient_variation_%` attribut **Bad Example (Brittle Automation):** This automation uses a hard-coded value. If you later change the "High" threshold in the integration's options to 35%, this automation will not respect that change and might trigger at the wrong time. + ```yaml automation: - - alias: "Brittle - Enable battery optimization" - trigger: - # - # BAD: Avoid hard-coding numeric values - # - - platform: numeric_state - entity_id: sensor._today_s_price_volatility - attribute: price_coefficient_variation_% - above: 30 - action: - - service: input_boolean.turn_on - entity_id: input_boolean.battery_optimization_enabled + - alias: "Brittle - Enable battery optimization" + trigger: + # + # BAD: Avoid hard-coding numeric values + # + - platform: numeric_state + entity_id: sensor._today_s_price_volatility + attribute: price_coefficient_variation_% + above: 30 + action: + - service: input_boolean.turn_on + entity_id: input_boolean.battery_optimization_enabled ``` By following the "Good Example", your automations become simpler, more readable, and much easier to maintain. @@ -319,19 +324,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -352,24 +357,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.28.0/actions.md b/docs/user/versioned_docs/version-v0.28.0/actions.md index 636879c..f0223a7 100644 --- a/docs/user/versioned_docs/version-v0.28.0/actions.md +++ b/docs/user/versioned_docs/version-v0.28.0/actions.md @@ -14,13 +14,13 @@ You can still call them from automations, scripts, and dashboards the same way a **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -52,14 +52,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -75,8 +75,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -125,6 +126,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -142,9 +144,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -152,9 +154,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -187,7 +189,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -195,7 +197,7 @@ response_variable: config type: custom:config-template-card entities: - binary_sensor._tomorrow_s_data_available - - sensor._chart_metadata # For dynamic Y-axis + - sensor._chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -212,6 +214,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -251,10 +254,10 @@ If you're still using the `sensor._chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.28.0/automation-examples.md b/docs/user/versioned_docs/version-v0.28.0/automation-examples.md index aec1d77..67c8ec1 100644 --- a/docs/user/versioned_docs/version-v0.28.0/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.28.0/automation-examples.md @@ -6,16 +6,17 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- > **Important Note:** The following examples are intended as templates to illustrate the logic. They are **not** suitable for direct copy & paste without adaptation. > > Please make sure you: +> > 1. Replace the **Entity IDs** (e.g., `sensor._...`, `switch.pool_pump`) with the IDs of your own devices and sensors. > 2. Adapt the logic to your specific devices (e.g., heat pump, EV, water boiler). > @@ -66,16 +67,15 @@ automation: - service: notify.mobile_app data: message: > - Home battery charging started. Price: {{ states('sensor._current_electricity_price') }} {{ state_attr('sensor._current_electricity_price', 'unit_of_measurement') }}. - Today's volatility is {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') }}. - + Home battery charging started. Price: {{ states('sensor._current_electricity_price') }} {{ state_attr('sensor._current_electricity_price', 'unit_of_measurement') }}. + Today's volatility is {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') }}. ``` **Why this works:** -- The automation only runs if volatility is `moderate`, `high`, or `very_high`. -- If you adjust your volatility thresholds in the future, this automation adapts automatically without any changes. -- It uses the `price_volatility` attribute, ensuring it works correctly regardless of your Home Assistant's display language. +- The automation only runs if volatility is `moderate`, `high`, or `very_high`. +- If you adjust your volatility thresholds in the future, this automation adapts automatically without any changes. +- It uses the `price_volatility` attribute, ensuring it works correctly regardless of your Home Assistant's display language. ### Use Case: Combined Volatility and Absolute Price Check @@ -118,9 +118,9 @@ automation: **Why this works:** -- On days with meaningful price swings, it charges during any `Best Price` period. -- On days with flat prices, it still charges if the price drops below your personal "cheap enough" threshold (e.g., 0.18 €/kWh or 18 ct/kWh). -- This gracefully handles midnight period flips, as the absolute price check will likely remain true if prices stay low. +- On days with meaningful price swings, it charges during any `Best Price` period. +- On days with flat prices, it still charges if the price drops below your personal "cheap enough" threshold (e.g., 0.18 €/kWh or 18 ct/kWh). +- This gracefully handles midnight period flips, as the absolute price check will likely remain true if prices stay low. ### Use Case: Using the Period's Own Volatility Attribute @@ -149,10 +149,10 @@ automation: **Why this works:** -- Each detected period has its own `volatility` attribute (`low`, `moderate`, etc.). -- This is the simplest way to check for meaningful savings for that specific period. -- The attribute name on the binary sensor is `volatility` (lowercase) and its value is also lowercase. -- It also contains other useful attributes like `price_mean`, `price_spread`, and the `price_coefficient_variation_%` for that period. +- Each detected period has its own `volatility` attribute (`low`, `moderate`, etc.). +- This is the simplest way to check for meaningful savings for that specific period. +- The attribute name on the binary sensor is `volatility` (lowercase) and its value is also lowercase. +- It also contains other useful attributes like `price_mean`, `price_spread`, and the `price_coefficient_variation_%` for that period. --- @@ -176,11 +176,11 @@ The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card **Required:** -- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS +- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** -- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS +- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -217,9 +217,9 @@ response_variable: apexcharts_config **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow -- **When tomorrow data not available**: Shows yesterday + today -- **Fixed 48h span:** Always shows full 48 hours +- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow +- **When tomorrow data not available**: Shows yesterday + today +- **Fixed 48h span:** Always shows full 48 hours **Auto-Zoom Variant:** @@ -234,17 +234,17 @@ data: response_variable: apexcharts_config ``` -- Same data loading as rolling window -- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight -- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight +- Same data loading as rolling window +- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight +- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight **Note:** Rolling window modes require Config Template Card to dynamically adjust the time range. ### Features -- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) -- Best price period highlighting (semi-transparent green overlay) -- Automatic NULL insertion for clean gaps -- Translated labels based on your Home Assistant language -- Interactive zoom and pan -- Live marker showing current time +- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) +- Best price period highlighting (semi-transparent green overlay) +- Automatic NULL insertion for clean gaps +- Translated labels based on your Home Assistant language +- Interactive zoom and pan +- Live marker showing current time diff --git a/docs/user/versioned_docs/version-v0.28.0/chart-examples.md b/docs/user/versioned_docs/version-v0.28.0/chart-examples.md index a782c50..abab858 100644 --- a/docs/user/versioned_docs/version-v0.28.0/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.28.0/chart-examples.md @@ -10,14 +10,15 @@ This guide showcases the different chart configurations available through the `t The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -31,6 +32,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -45,6 +47,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -61,6 +64,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -75,6 +79,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -82,6 +87,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -95,6 +101,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -109,6 +116,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -116,6 +124,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -157,6 +166,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -168,6 +178,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -193,6 +204,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -207,6 +219,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -219,18 +232,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -245,9 +258,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -262,7 +275,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -294,14 +307,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.28.0/concepts.md b/docs/user/versioned_docs/version-v0.28.0/concepts.md index 2a95bed..1822276 100644 --- a/docs/user/versioned_docs/version-v0.28.0/concepts.md +++ b/docs/user/versioned_docs/version-v0.28.0/concepts.md @@ -21,6 +21,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -33,6 +34,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -53,6 +55,7 @@ This helps you understand if current prices are exceptional or typical. ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -62,6 +65,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.28.0/configuration.md b/docs/user/versioned_docs/version-v0.28.0/configuration.md index 0b88a25..1e45eec 100644 --- a/docs/user/versioned_docs/version-v0.28.0/configuration.md +++ b/docs/user/versioned_docs/version-v0.28.0/configuration.md @@ -19,12 +19,14 @@ The integration allows you to choose how average price sensors display their val #### Display Modes **Median (Default):** + - Shows the "middle value" when all prices are sorted - **Resistant to extreme spikes** - one expensive hour doesn't skew the result - Best for understanding **typical price levels** - Example: "What was the typical price today?" **Arithmetic Mean:** + - Shows the mathematical average of all prices - **Includes effect of spikes** - reflects actual cost if consuming evenly - Best for **cost calculations and budgeting** @@ -33,6 +35,7 @@ The integration allows you to choose how average price sensors display their val #### Why This Matters Consider a day with these hourly prices: + ``` 10, 12, 13, 15, 80 ct/kWh ``` @@ -53,6 +56,7 @@ The median tells you the price was **typically** around 13 ct/kWh (4 out of 5 ho ``` This means: + - ✅ You can change the display anytime without breaking automations - ✅ Automations can use both values for different purposes - ✅ No need to create template sensors for the "other" value @@ -60,6 +64,7 @@ This means: #### Affected Sensors This setting applies to: + - Daily average sensors (today, tomorrow) - 24-hour rolling averages (trailing, leading) - Hourly smoothed prices (current hour, next hour) @@ -70,12 +75,14 @@ See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examp #### Choosing Your Display **Choose Median if:** + - 👥 You show prices to users ("What's today like?") - 📊 You want dashboard values that represent typical conditions - 🎯 You compare price levels across days - 🔍 You analyze volatility (comparing typical vs extremes) **Choose Mean if:** + - 💰 You calculate costs and budgets - 📈 You forecast energy expenses - 🧮 You need mathematical accuracy for financial planning @@ -93,27 +100,27 @@ When enabled, these entities override the corresponding Options Flow settings: #### Best Price Period Settings -| Entity | Type | Range | Description | -|--------|------|-------|-------------| -| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals | -| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average | -| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold | -| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Entity | Type | Range | Description | +| ------------------------------------- | ------ | ---------- | ------------------------------------------------------ | +| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals | +| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average | +| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | +| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | +| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | +| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold | +| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | #### Peak Price Period Settings -| Entity | Type | Range | Description | -|--------|------|-------|-------------| -| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | -| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | -| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | -| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Entity | Type | Range | Description | +| ------------------------------------- | ------ | ---------- | ------------------------------------------------------ | +| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | +| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | +| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | +| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | +| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | +| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | +| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | ### How Runtime Overrides Work @@ -138,24 +145,25 @@ Each configuration entity includes a detailed description attribute explaining w ```yaml automation: - - alias: "Winter: Stricter Best Price Detection" - trigger: - - platform: time - at: "00:00:00" - condition: - - condition: template - value_template: "{{ now().month in [11, 12, 1, 2] }}" - action: - - service: number.set_value - target: - entity_id: number._best_price_flexibility - data: - value: 10 # Stricter than default 15% + - alias: "Winter: Stricter Best Price Detection" + trigger: + - platform: time + at: "00:00:00" + condition: + - condition: template + value_template: "{{ now().month in [11, 12, 1, 2] }}" + action: + - service: number.set_value + target: + entity_id: number._best_price_flexibility + data: + value: 10 # Stricter than default 15% ``` ### Recorder Optimization (Optional) These configuration entities are designed to minimize database impact: + - **EntityCategory.CONFIG** - Excluded from Long-Term Statistics - All attributes excluded from history recording - Only state value changes are recorded @@ -166,16 +174,17 @@ However, if you prefer to **completely exclude** these entities from the recorde ```yaml recorder: - exclude: - entity_globs: - # Exclude all Tibber Prices configuration entities - - number.*_best_price_* - - number.*_peak_price_* - - switch.*_best_price_* - - switch.*_peak_price_* + exclude: + entity_globs: + # Exclude all Tibber Prices configuration entities + - number.*_best_price_* + - number.*_peak_price_* + - switch.*_best_price_* + - switch.*_peak_price_* ``` This is especially useful if: + - You rarely change these settings - You want the smallest possible database footprint - You don't need to see the history graph for these entities diff --git a/docs/user/versioned_docs/version-v0.28.0/dashboard-examples.md b/docs/user/versioned_docs/version-v0.28.0/dashboard-examples.md index 777b059..9fd206b 100644 --- a/docs/user/versioned_docs/version-v0.28.0/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.28.0/dashboard-examples.md @@ -12,13 +12,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor._current_electricity_price - name: Current Price - icon: mdi:flash - - entity: sensor._current_price_rating - name: Price Rating - - entity: sensor._next_electricity_price - name: Next Price + - entity: sensor._current_electricity_price + name: Current Price + icon: mdi:flash + - entity: sensor._current_price_rating + name: Price Rating + - entity: sensor._next_electricity_price + name: Next Price ``` ## Period Status Cards @@ -28,14 +28,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor._best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor._peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor._best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor._peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -48,16 +48,16 @@ entity: sensor._current_price_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -69,19 +69,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor._current_electricity_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor._current_electricity_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor._best_price_start - name: Best Period Starts - - entity: binary_sensor._best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor._best_price_start + name: Best Period Starts + - entity: binary_sensor._best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -93,25 +93,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor._current_electricity_price - - sensor._current_price_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor._current_electricity_price + - sensor._current_price_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor._price_today - - sensor._today_s_lowest_price - - sensor._today_s_highest_price + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor._price_today + - sensor._today_s_lowest_price + - sensor._today_s_highest_price ``` ## Icon Color Integration @@ -121,17 +121,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor._current_electricity_price - icon_color: "{{ state_attr('sensor._current_electricity_price', 'icon_color') }}" + - type: entity + entity: sensor._current_electricity_price + icon_color: "{{ state_attr('sensor._current_electricity_price', 'icon_color') }}" - - type: entity - entity: binary_sensor._best_price_period - icon_color: green + - type: entity + entity: binary_sensor._best_price_period + icon_color: green - - type: entity - entity: binary_sensor._peak_price_period - icon_color: red + - type: entity + entity: binary_sensor._peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -144,21 +144,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor._current_electricity_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor._current_electricity_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor._best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor._best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -168,21 +168,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor._*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor._*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.28.0/dynamic-icons.md b/docs/user/versioned_docs/version-v0.28.0/dynamic-icons.md index ed53f24..3be732d 100644 --- a/docs/user/versioned_docs/version-v0.28.0/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.28.0/dynamic-icons.md @@ -8,10 +8,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -26,10 +26,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_price_level`) -- Price rating sensors (e.g., `current_price_rating`) -- Volatility sensors (e.g., `today_s_price_volatility`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_price_level`) +- Price rating sensors (e.g., `current_price_rating`) +- Volatility sensors (e.g., `today_s_price_volatility`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -125,8 +125,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -134,24 +134,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor._best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -159,22 +159,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.28.0/faq.md b/docs/user/versioned_docs/version-v0.28.0/faq.md index 260b09d..7cf4220 100644 --- a/docs/user/versioned_docs/version-v0.28.0/faq.md +++ b/docs/user/versioned_docs/version-v0.28.0/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -118,18 +128,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor._best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor._best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -140,19 +150,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor._peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor._peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.28.0/glossary.md b/docs/user/versioned_docs/version-v0.28.0/glossary.md index 2822918..cc35e91 100644 --- a/docs/user/versioned_docs/version-v0.28.0/glossary.md +++ b/docs/user/versioned_docs/version-v0.28.0/glossary.md @@ -100,6 +100,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.28.0/icon-colors.md b/docs/user/versioned_docs/version-v0.28.0/icon-colors.md index 8e1a868..0e17a00 100644 --- a/docs/user/versioned_docs/version-v0.28.0/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.28.0/icon-colors.md @@ -16,17 +16,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -40,12 +40,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_price_level`) -- Price rating sensors (e.g., `current_price_rating`) -- Volatility sensors (e.g., `today_s_price_volatility`) -- Price trend sensors (e.g., `price_trend_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_price_level`) +- Price rating sensors (e.g., `current_price_rating`) +- Volatility sensors (e.g., `today_s_price_volatility`) +- Price trend sensors (e.g., `price_trend_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -53,15 +53,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -297,12 +297,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -428,22 +428,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.28.0/intro.md b/docs/user/versioned_docs/version-v0.28.0/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.28.0/intro.md +++ b/docs/user/versioned_docs/version-v0.28.0/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.28.0/period-calculation.md b/docs/user/versioned_docs/version-v0.28.0/period-calculation.md index 57480f0..d58f92b 100644 --- a/docs/user/versioned_docs/version-v0.28.0/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.28.0/period-calculation.md @@ -6,19 +6,19 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [Fewer Periods Than Configured](#fewer-periods-than-configured) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Understanding Sensor Attributes](#understanding-sensor-attributes) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [Fewer Periods Than Configured](#fewer-periods-than-configured) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Understanding Sensor Attributes](#understanding-sensor-attributes) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) +- [Advanced Topics](#advanced-topics) --- @@ -28,8 +28,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -47,16 +47,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -136,7 +139,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -150,9 +153,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -192,8 +196,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -210,8 +214,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -226,8 +230,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -270,9 +274,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -306,7 +310,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -316,7 +320,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -355,11 +359,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -371,6 +377,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -404,10 +411,10 @@ Flex 18% + Level=any → SUCCESS! Found 2 periods ✓ ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -440,9 +447,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -473,7 +480,7 @@ This is **expected behavior** on days with very uniform electricity prices. When ```yaml min_periods_configured: 2 periods_found_total: 1 -flat_days_detected: 1 # Uniform prices today → 1 period is the right answer +flat_days_detected: 1 # Uniform prices today → 1 period is the right answer ``` You don't need to change anything. This is the integration protecting you from artificial periods. @@ -483,14 +490,16 @@ You don't need to change anything. This is the integration protecting you from a Relaxation tried all configured attempts but couldn't reach your target. Options: 1. **Increase relaxation attempts** (tries more flexibility levels before giving up) - ```yaml - relaxation_attempts_best: 12 # Default: 11 - ``` + + ```yaml + relaxation_attempts_best: 12 # Default: 11 + ``` 2. **Reduce minimum period count** - ```yaml - min_periods_best: 1 # Only require 1 period per day - ``` + + ```yaml + min_periods_best: 1 # Only require 1 period per day + ``` 3. **Check filter settings** – very strict `best_price_min_distance_from_avg` values block relaxation @@ -503,24 +512,26 @@ Relaxation tried all configured attempts but couldn't reach your target. Options **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -529,20 +540,22 @@ Relaxation tried all configured attempts but couldn't reach your target. Options **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -552,26 +565,26 @@ Relaxation tried all configured attempts but couldn't reach your target. Options # Entity: binary_sensor._best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Calculation summary (always shown – diagnostic overview of this calculation run): -min_periods_configured: 2 # What you configured as target -periods_found_total: 3 # What was actually found across all days +min_periods_configured: 2 # What you configured as target +periods_found_total: 3 # What was actually found across all days # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated -flat_days_detected: 1 # Days where prices were so flat that 1 period is enough -relaxation_incomplete: true # Some days couldn't reach the configured target +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +flat_days_detected: 1 # Days where prices were so flat that 1 period is enough +relaxation_incomplete: true # Some days couldn't reach the configured target ``` #### What the diagnostic attributes mean @@ -581,13 +594,13 @@ relaxation_incomplete: true # Some days couldn't reach the configured ta These two values together quickly show whether the calculation achieved its goal: ```yaml -min_periods_configured: 2 # You asked for 2 periods per day -periods_found_total: 6 # 3 days × 2 periods = fully satisfied ✅ +min_periods_configured: 2 # You asked for 2 periods per day +periods_found_total: 6 # 3 days × 2 periods = fully satisfied ✅ ``` ```yaml min_periods_configured: 2 -periods_found_total: 5 # 3 days, but one day got only 1 period +periods_found_total: 5 # 3 days, but one day got only 1 period ``` Note that `periods_found_total` counts **all periods across today and tomorrow** – so 4 on a two-day view means 2 per day on average. @@ -599,7 +612,7 @@ This is the most important diagnostic for days with very uniform prices (e.g. su ```yaml min_periods_configured: 2 periods_found_total: 1 -flat_days_detected: 1 # ← This explains why you got 1 instead of 2 +flat_days_detected: 1 # ← This explains why you got 1 instead of 2 ``` When prices barely change across the day – typically a variation of less than 10% – the integration automatically reduces the target from your configured value to 1. There is no meaningful second "best price window" when all prices are essentially equal. @@ -613,10 +626,11 @@ This flag appears when even after all relaxation attempts, at least one day coul ```yaml min_periods_configured: 2 periods_found_total: 1 -relaxation_incomplete: true # ← Relaxation tried everything, still short +relaxation_incomplete: true # ← Relaxation tried everything, still short ``` This is most common on very flat days (see above) or with very strict filter settings. If you see this repeatedly on normal days, consider: + - Reducing `min_periods_best` to 1 - Increasing `relaxation_attempts_best` - Checking if your `best_price_min_distance_from_avg` is too high @@ -630,12 +644,14 @@ This is most common on very flat days (see above) or with very strict filter set This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -661,6 +677,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -671,8 +688,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor._today_s_price_volatility: 8.2% # Low volatility -sensor._tomorrow_s_price_volatility: 7.9% # Also low +sensor._today_s_price_volatility: 8.2% # Low volatility +sensor._tomorrow_s_price_volatility: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -738,15 +756,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor._best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -757,8 +776,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -783,11 +802,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.28.0/sensors.md b/docs/user/versioned_docs/version-v0.28.0/sensors.md index 995b26e..6925624 100644 --- a/docs/user/versioned_docs/version-v0.28.0/sensors.md +++ b/docs/user/versioned_docs/version-v0.28.0/sensors.md @@ -18,8 +18,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -31,15 +31,15 @@ The integration provides several sensors that calculate average electricity pric #### Available Average Sensors -| Sensor | Description | Time Window | -|--------|-------------|-------------| -| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | -| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | -| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | -| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | -| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | -| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | -| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | +| Sensor | Description | Time Window | +| -------------------------- | -------------------------------------- | ------------------------------- | +| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | +| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | +| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | +| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | +| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | +| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | +| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | #### Configurable Display: Median vs Mean @@ -68,8 +68,8 @@ You can choose which value is displayed in the sensor state: 2. Click **Configure** on your home 3. Navigate to **Step 6: Average Sensor Display Settings** 4. Choose between: - - **Median** (default) - Shows typical price level, resistant to spikes - - **Arithmetic Mean** - Shows actual mathematical average + - **Median** (default) - Shows typical price level, resistant to spikes + - **Arithmetic Mean** - Shows actual mathematical average **Important:** Both values are **always available** as sensor attributes, regardless of your choice! This ensures your automations continue to work if you change the display setting. @@ -80,22 +80,22 @@ Both `price_mean` and `price_median` are always available as attributes: ```yaml # Example: Get both values regardless of display setting sensor: - - platform: template - sensors: - daily_price_analysis: - friendly_name: "Daily Price Analysis" - value_template: > - {% set median = state_attr('sensor._price_today', 'price_median') %} - {% set mean = state_attr('sensor._price_today', 'price_mean') %} - {% set current = states('sensor._current_electricity_price') | float %} + - platform: template + sensors: + daily_price_analysis: + friendly_name: "Daily Price Analysis" + value_template: > + {% set median = state_attr('sensor._price_today', 'price_median') %} + {% set mean = state_attr('sensor._price_today', 'price_mean') %} + {% set current = states('sensor._current_electricity_price') | float %} - {% if current < median %} - Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) - {% elif current < mean %} - Typical price range - {% else %} - Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) - {% endif %} + {% if current < median %} + Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) + {% elif current < mean %} + Typical price range + {% else %} + Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) + {% endif %} ``` #### Practical Examples @@ -106,21 +106,21 @@ Run dishwasher only when price is significantly below the daily typical level: ```yaml automation: - - alias: "Start Dishwasher When Cheap" - trigger: - - platform: state - entity_id: binary_sensor._best_price_period - to: "on" - condition: - # Only if current price is at least 20% below typical (median) - - condition: template - value_template: > - {% set current = states('sensor._current_electricity_price') | float %} - {% set median = state_attr('sensor._price_today', 'price_median') | float %} - {{ current < (median * 0.8) }} - action: - - service: switch.turn_on - entity_id: switch.dishwasher + - alias: "Start Dishwasher When Cheap" + trigger: + - platform: state + entity_id: binary_sensor._best_price_period + to: "on" + condition: + # Only if current price is at least 20% below typical (median) + - condition: template + value_template: > + {% set current = states('sensor._current_electricity_price') | float %} + {% set median = state_attr('sensor._price_today', 'price_median') | float %} + {{ current < (median * 0.8) }} + action: + - service: switch.turn_on + entity_id: switch.dishwasher ``` **Example 2: Cost-aware heating control** @@ -129,20 +129,20 @@ Use mean for actual cost calculations: ```yaml automation: - - alias: "Heating Budget Control" - trigger: - - platform: time - at: "06:00:00" - action: - # Calculate expected daily heating cost - - variables: - mean_price: "{{ state_attr('sensor._price_today', 'price_mean') | float }}" - heating_kwh_per_day: 15 # Estimated consumption - daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" - - service: notify.mobile_app - data: - title: "Heating Cost Estimate" - message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" + - alias: "Heating Budget Control" + trigger: + - platform: time + at: "06:00:00" + action: + # Calculate expected daily heating cost + - variables: + mean_price: "{{ state_attr('sensor._price_today', 'price_mean') | float }}" + heating_kwh_per_day: 15 # Estimated consumption + daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" + - service: notify.mobile_app + data: + title: "Heating Cost Estimate" + message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" ``` **Example 3: Smart charging based on rolling average** @@ -151,60 +151,61 @@ Use trailing average to understand recent price trends: ```yaml automation: - - alias: "EV Charging - Price Trend Based" - trigger: - - platform: state - entity_id: sensor.ev_battery_level - condition: - # Start charging if current price < 90% of recent 24h average - - condition: template - value_template: > - {% set current = states('sensor._current_electricity_price') | float %} - {% set trailing_avg = state_attr('sensor._price_trailing_24h', 'price_median') | float %} - {{ current < (trailing_avg * 0.9) }} - # And battery < 80% - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - action: - - service: switch.turn_on - entity_id: switch.ev_charger + - alias: "EV Charging - Price Trend Based" + trigger: + - platform: state + entity_id: sensor.ev_battery_level + condition: + # Start charging if current price < 90% of recent 24h average + - condition: template + value_template: > + {% set current = states('sensor._current_electricity_price') | float %} + {% set trailing_avg = state_attr('sensor._price_trailing_24h', 'price_median') | float %} + {{ current < (trailing_avg * 0.9) }} + # And battery < 80% + - condition: numeric_state + entity_id: sensor.ev_battery_level + below: 80 + action: + - service: switch.turn_on + entity_id: switch.ev_charger ``` #### Key Attributes All average sensors provide these attributes: -| Attribute | Description | Example | -|-----------|-------------|---------| -| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | -| `price_median` | Median value (always available) | 22.1 ct/kWh | -| `interval_count` | Number of intervals included | 96 | -| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | +| Attribute | Description | Example | +| ---------------- | ---------------------------------- | ------------------------- | +| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | +| `price_median` | Median value (always available) | 22.1 ct/kWh | +| `interval_count` | Number of intervals included | 96 | +| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | **Note:** The `price_mean` and `price_median` attributes are **always present** regardless of which value you configured for display. This ensures automation compatibility when changing the display setting. #### When to Use Which Value **Use Median for:** + - ✅ Comparing "typical" price levels across days - ✅ Determining if current price is unusually high/low - ✅ User-facing displays ("What was today like?") - ✅ Volatility analysis (comparing typical vs extremes) **Use Mean for:** + - ✅ Cost calculations and budgeting - ✅ Energy cost estimations - ✅ Comparing actual average costs between periods - ✅ Financial planning and forecasting **Both values tell different stories:** + - High median + much higher mean = Expensive spikes occurred - Low median + higher mean = Generally cheap with occasional spikes - Similar median and mean = Stable prices (low volatility) - - ## Volatility Sensors Volatility sensors help you understand how much electricity prices fluctuate over a given period. Instead of just looking at the absolute price, they measure the **relative price variation**, which is a great indicator of whether it's a good day for price-based energy optimization. @@ -219,21 +220,23 @@ The sensor's state can be `low`, `moderate`, `high`, or `very_high`, based on co ### Available Volatility Sensors -| Sensor | Description | Time Window | -|---|---|---| -| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | -| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | -| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | -| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | +| Sensor | Description | Time Window | +| ------------------------------------- | ----------------------------------------- | ---------------------- | +| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | +| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | +| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | +| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | ### Configuration You can adjust the CV thresholds that determine the volatility level: + 1. Go to **Settings → Devices & Services → Tibber Prices**. 2. Click **Configure**. 3. Go to the **Price Volatility Thresholds** step. Default thresholds are: + - **Moderate:** 15% - **High:** 30% - **Very High:** 50% @@ -242,14 +245,14 @@ Default thresholds are: All volatility sensors provide these attributes: -| Attribute | Description | Example | -|---|---|---| -| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` | -| `price_spread` | The difference between the highest and lowest price | `12.3` | -| `price_min` | The lowest price in the period | `10.2` | -| `price_max` | The highest price in the period | `22.5` | -| `price_mean` | The arithmetic mean of all prices in the period | `15.1` | -| `interval_count` | Number of price intervals included in the calculation | `96` | +| Attribute | Description | Example | +| ------------------------------- | ----------------------------------------------------- | ------- | +| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` | +| `price_spread` | The difference between the highest and lowest price | `12.3` | +| `price_min` | The lowest price in the period | `10.2` | +| `price_max` | The highest price in the period | `22.5` | +| `price_mean` | The arithmetic mean of all prices in the period | `15.1` | +| `interval_count` | Number of price intervals included in the calculation | `96` | ### Usage in Automations & Best Practices @@ -263,16 +266,17 @@ For automations, it is strongly recommended to use the `price_volatility` attrib **Good Example (Robust Automation):** This automation triggers only if the volatility is classified as `high` or `very_high`, respecting your central settings and working independently of the system language. + ```yaml automation: - - alias: "Enable battery optimization only on volatile days" - trigger: - - platform: template - value_template: > - {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }} - action: - - service: input_boolean.turn_on - entity_id: input_boolean.battery_optimization_enabled + - alias: "Enable battery optimization only on volatile days" + trigger: + - platform: template + value_template: > + {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }} + action: + - service: input_boolean.turn_on + entity_id: input_boolean.battery_optimization_enabled ``` --- @@ -285,20 +289,21 @@ You might be tempted to use the numeric `price_coefficient_variation_%` attribut **Bad Example (Brittle Automation):** This automation uses a hard-coded value. If you later change the "High" threshold in the integration's options to 35%, this automation will not respect that change and might trigger at the wrong time. + ```yaml automation: - - alias: "Brittle - Enable battery optimization" - trigger: - # - # BAD: Avoid hard-coding numeric values - # - - platform: numeric_state - entity_id: sensor._today_s_price_volatility - attribute: price_coefficient_variation_% - above: 30 - action: - - service: input_boolean.turn_on - entity_id: input_boolean.battery_optimization_enabled + - alias: "Brittle - Enable battery optimization" + trigger: + # + # BAD: Avoid hard-coding numeric values + # + - platform: numeric_state + entity_id: sensor._today_s_price_volatility + attribute: price_coefficient_variation_% + above: 30 + action: + - service: input_boolean.turn_on + entity_id: input_boolean.battery_optimization_enabled ``` By following the "Good Example", your automations become simpler, more readable, and much easier to maintain. @@ -319,19 +324,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -352,24 +357,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.29.0/actions.md b/docs/user/versioned_docs/version-v0.29.0/actions.md index 59e5255..926f307 100644 --- a/docs/user/versioned_docs/version-v0.29.0/actions.md +++ b/docs/user/versioned_docs/version-v0.29.0/actions.md @@ -38,13 +38,13 @@ If you have configured more than one Tibber home, each home has its own entry ID **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -76,14 +76,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -99,8 +99,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -149,6 +150,7 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -166,9 +168,9 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -176,9 +178,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -211,7 +213,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -219,7 +221,7 @@ response_variable: config type: custom:config-template-card entities: - binary_sensor._tomorrow_s_data_available - - sensor._chart_metadata # For dynamic Y-axis + - sensor._chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -236,6 +238,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -275,10 +278,10 @@ If you're still using the `sensor._chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.29.0/automation-examples.md b/docs/user/versioned_docs/version-v0.29.0/automation-examples.md index c82e0aa..3d48598 100644 --- a/docs/user/versioned_docs/version-v0.29.0/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.29.0/automation-examples.md @@ -4,16 +4,17 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- > **Important Note:** The following examples are intended as templates to illustrate the logic. They are **not** suitable for direct copy & paste without adaptation. > > Please make sure you: +> > 1. Replace the **Entity IDs** (e.g., `sensor._...`, `switch.pool_pump`) with the IDs of your own devices and sensors. > 2. Adapt the logic to your specific devices (e.g., heat pump, EV, water boiler). > @@ -254,14 +255,14 @@ A common misconception: **"rising" does NOT mean "too late"**. It means your cur ### Sensor Combination Quick Reference -| What You Want | Sensors to Combine | -|---|---| -| **"Is it cheap right now?"** | `rating_level` attribute (VERY_CHEAP, CHEAP) | -| **"Will prices go up or down?"** | `current_price_trend` state (falling/stable/rising) | -| **"When will the trend change?"** | `next_price_trend_change` state (timestamp) | -| **"How cheap will it get?"** | `next_Nh_avg` attribute on trend sensors | -| **"Is the price drop meaningful?"** | `today_s_price_volatility` (not low = meaningful) | -| **"Ride the full cheap wave"** | `rating_level` + `current_price_trend` + `best_price_period` | +| What You Want | Sensors to Combine | +| ----------------------------------- | ------------------------------------------------------------ | +| **"Is it cheap right now?"** | `rating_level` attribute (VERY_CHEAP, CHEAP) | +| **"Will prices go up or down?"** | `current_price_trend` state (falling/stable/rising) | +| **"When will the trend change?"** | `next_price_trend_change` state (timestamp) | +| **"How cheap will it get?"** | `next_Nh_avg` attribute on trend sensors | +| **"Is the price drop meaningful?"** | `today_s_price_volatility` (not low = meaningful) | +| **"Ride the full cheap wave"** | `rating_level` + `current_price_trend` + `best_price_period` | --- @@ -302,16 +303,15 @@ automation: - service: notify.mobile_app data: message: > - Home battery charging started. Price: {{ states('sensor._current_electricity_price') }} {{ state_attr('sensor._current_electricity_price', 'unit_of_measurement') }}. - Today's volatility is {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') }}. - + Home battery charging started. Price: {{ states('sensor._current_electricity_price') }} {{ state_attr('sensor._current_electricity_price', 'unit_of_measurement') }}. + Today's volatility is {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') }}. ``` **Why this works:** -- The automation only runs if volatility is `moderate`, `high`, or `very_high`. -- If you adjust your volatility thresholds in the future, this automation adapts automatically without any changes. -- It uses the `price_volatility` attribute, ensuring it works correctly regardless of your Home Assistant's display language. +- The automation only runs if volatility is `moderate`, `high`, or `very_high`. +- If you adjust your volatility thresholds in the future, this automation adapts automatically without any changes. +- It uses the `price_volatility` attribute, ensuring it works correctly regardless of your Home Assistant's display language. ### Use Case: Combined Volatility and Absolute Price Check @@ -354,9 +354,9 @@ automation: **Why this works:** -- On days with meaningful price swings, it charges during any `Best Price` period. -- On days with flat prices, it still charges if the price drops below your personal "cheap enough" threshold (e.g., 0.18 €/kWh or 18 ct/kWh). -- This gracefully handles midnight period flips, as the absolute price check will likely remain true if prices stay low. +- On days with meaningful price swings, it charges during any `Best Price` period. +- On days with flat prices, it still charges if the price drops below your personal "cheap enough" threshold (e.g., 0.18 €/kWh or 18 ct/kWh). +- This gracefully handles midnight period flips, as the absolute price check will likely remain true if prices stay low. ### Use Case: Using the Period's Own Volatility Attribute @@ -385,10 +385,10 @@ automation: **Why this works:** -- Each detected period has its own `volatility` attribute (`low`, `moderate`, etc.). -- This is the simplest way to check for meaningful savings for that specific period. -- The attribute name on the binary sensor is `volatility` (lowercase) and its value is also lowercase. -- It also contains other useful attributes like `price_mean`, `price_spread`, and the `price_coefficient_variation_%` for that period. +- Each detected period has its own `volatility` attribute (`low`, `moderate`, etc.). +- This is the simplest way to check for meaningful savings for that specific period. +- The attribute name on the binary sensor is `volatility` (lowercase) and its value is also lowercase. +- It also contains other useful attributes like `price_mean`, `price_spread`, and the `price_coefficient_variation_%` for that period. --- @@ -414,9 +414,9 @@ automation: action: # Compare different future windows to find cheapest start - variables: - next_2h: "{{ state_attr('sensor._price_trend_2h', 'next_2h_avg') | float(999) }}" - next_4h: "{{ state_attr('sensor._price_trend_4h', 'next_4h_avg') | float(999) }}" - daily_avg: "{{ state_attr('sensor._price_today', 'price_median') | float(999) }}" + next_2h: "{{ state_attr('sensor._price_trend_2h', 'next_2h_avg') | float(999) }}" + next_4h: "{{ state_attr('sensor._price_trend_4h', 'next_4h_avg') | float(999) }}" + daily_avg: "{{ state_attr('sensor._price_today', 'price_median') | float(999) }}" - service: notify.mobile_app data: title: "Dishwasher Scheduling" @@ -479,11 +479,11 @@ The examples below contain `entry_id: YOUR_ENTRY_ID`. This value identifies whic **Required:** -- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS +- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** -- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS +- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -520,9 +520,9 @@ response_variable: apexcharts_config **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow -- **When tomorrow data not available**: Shows yesterday + today -- **Fixed 48h span:** Always shows full 48 hours +- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow +- **When tomorrow data not available**: Shows yesterday + today +- **Fixed 48h span:** Always shows full 48 hours **Auto-Zoom Variant:** @@ -537,17 +537,17 @@ data: response_variable: apexcharts_config ``` -- Same data loading as rolling window -- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight -- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight +- Same data loading as rolling window +- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight +- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight **Note:** Rolling window modes require Config Template Card to dynamically adjust the time range. ### Features -- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) -- Best price period highlighting (semi-transparent green overlay) -- Automatic NULL insertion for clean gaps -- Translated labels based on your Home Assistant language -- Interactive zoom and pan -- Live marker showing current time +- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) +- Best price period highlighting (semi-transparent green overlay) +- Automatic NULL insertion for clean gaps +- Translated labels based on your Home Assistant language +- Interactive zoom and pan +- Live marker showing current time diff --git a/docs/user/versioned_docs/version-v0.29.0/chart-examples.md b/docs/user/versioned_docs/version-v0.29.0/chart-examples.md index 2648378..635fe14 100644 --- a/docs/user/versioned_docs/version-v0.29.0/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.29.0/chart-examples.md @@ -18,14 +18,15 @@ Every example below contains `entry_id: YOUR_ENTRY_ID`. This value identifies wh The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -39,6 +40,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -53,6 +55,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -69,6 +72,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -83,6 +87,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -90,6 +95,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -103,6 +109,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -117,6 +124,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -124,6 +132,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -165,6 +174,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -176,6 +186,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -201,6 +212,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -215,6 +227,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -227,18 +240,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -253,9 +266,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -270,7 +283,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -302,14 +315,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.29.0/concepts.md b/docs/user/versioned_docs/version-v0.29.0/concepts.md index f0befd2..e650261 100644 --- a/docs/user/versioned_docs/version-v0.29.0/concepts.md +++ b/docs/user/versioned_docs/version-v0.29.0/concepts.md @@ -57,6 +57,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -69,6 +70,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -100,6 +102,7 @@ See [Automation Examples → V-Shaped Days](automation-examples.md#understanding ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -109,6 +112,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.29.0/configuration.md b/docs/user/versioned_docs/version-v0.29.0/configuration.md index 0ed5d9d..e51d855 100644 --- a/docs/user/versioned_docs/version-v0.29.0/configuration.md +++ b/docs/user/versioned_docs/version-v0.29.0/configuration.md @@ -63,12 +63,12 @@ All steps have sensible defaults — you can click through without changes and f Configure how the integration classifies prices relative to the 24-hour trailing average: -| Setting | Default | Description | -|---------|---------|-------------| -| **Low threshold** | -10% | Prices this much below average → **LOW** rating | -| **High threshold** | +10% | Prices this much above average → **HIGH** rating | -| **Hysteresis** | 2% | Prevents flickering at threshold boundaries | -| **Gap tolerance** | 1 | Smooth isolated rating blocks (e.g., lone NORMAL between two LOWs) | +| Setting | Default | Description | +| ------------------ | ------- | ------------------------------------------------------------------ | +| **Low threshold** | -10% | Prices this much below average → **LOW** rating | +| **High threshold** | +10% | Prices this much above average → **HIGH** rating | +| **Hysteresis** | 2% | Prevents flickering at threshold boundaries | +| **Gap tolerance** | 1 | Smooth isolated rating blocks (e.g., lone NORMAL between two LOWs) | ### Step 4: Price Level Gap Tolerance @@ -80,26 +80,29 @@ Configure how the integration classifies prices relative to the 24-hour trailing Configure the Coefficient of Variation (CV) boundaries: -| Level | Default | Meaning | -|-------|---------|---------| -| **Moderate** | 15% | Noticeable price variation, some optimization potential | -| **High** | 30% | Significant price swings, good for timing optimization | -| **Very High** | 50% | Extreme volatility, maximum optimization benefit | +| Level | Default | Meaning | +| ------------- | ------- | ------------------------------------------------------- | +| **Moderate** | 15% | Noticeable price variation, some optimization potential | +| **High** | 30% | Significant price swings, good for timing optimization | +| **Very High** | 50% | Extreme volatility, maximum optimization benefit | ### Step 6: Best Price Period Configure detection of favorable price windows. Three collapsible sections: **Period Settings:** + - Minimum period length (default: 30 min) - Maximum price level to include (default: CHEAP) - Gap tolerance: how many expensive intervals to bridge (default: 1) **Flexibility Settings:** + - Flex percentage (default: 15%): how far above the daily minimum a price can be to qualify - Minimum distance from daily average (default: 5%): ensures periods are meaningfully cheaper **Relaxation & Target:** + - Enable minimum period target (default: on) - Target periods per day (default: 2) - Relaxation attempts (default: 11): steps to loosen criteria if target not met @@ -114,12 +117,12 @@ Mirrors Best Price configuration but for expensive windows. Detects periods to * Configure when trend sensors report rising/falling: -| Setting | Default | Description | -|---------|---------|-------------| -| **Rising** | 3% | Future average this much above current → "rising" | -| **Strongly rising** | 9% | Future average far above current → "strongly_rising" | -| **Falling** | -3% | Future average this much below current → "falling" | -| **Strongly falling** | -9% | Future average far below current → "strongly_falling" | +| Setting | Default | Description | +| -------------------- | ------- | ----------------------------------------------------- | +| **Rising** | 3% | Future average this much above current → "rising" | +| **Strongly rising** | 9% | Future average far above current → "strongly_rising" | +| **Falling** | -3% | Future average this much below current → "falling" | +| **Strongly falling** | -9% | Future average far below current → "strongly_falling" | Thresholds are [volatility-adaptive](sensors.md#trend-sensors): automatically widened on volatile days to prevent constant state changes. @@ -138,12 +141,14 @@ The integration allows you to choose how average price sensors display their val #### Display Modes **Median (Default):** + - Shows the "middle value" when all prices are sorted - **Resistant to extreme spikes** - one expensive hour doesn't skew the result - Best for understanding **typical price levels** - Example: "What was the typical price today?" **Arithmetic Mean:** + - Shows the mathematical average of all prices - **Includes effect of spikes** - reflects actual cost if consuming evenly - Best for **cost calculations and budgeting** @@ -152,6 +157,7 @@ The integration allows you to choose how average price sensors display their val #### Why This Matters Consider a day with these hourly prices: + ``` 10, 12, 13, 15, 80 ct/kWh ``` @@ -172,6 +178,7 @@ The median tells you the price was **typically** around 13 ct/kWh (4 out of 5 ho ``` This means: + - ✅ You can change the display anytime without breaking automations - ✅ Automations can use both values for different purposes - ✅ No need to create template sensors for the "other" value @@ -179,6 +186,7 @@ This means: #### Affected Sensors This setting applies to: + - Daily average sensors (today, tomorrow) - 24-hour rolling averages (trailing, leading) - Hourly smoothed prices (current hour, next hour) @@ -189,12 +197,14 @@ See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examp #### Choosing Your Display **Choose Median if:** + - 👥 You show prices to users ("What's today like?") - 📊 You want dashboard values that represent typical conditions - 🎯 You compare price levels across days - 🔍 You analyze volatility (comparing typical vs extremes) **Choose Mean if:** + - 💰 You calculate costs and budgets - 📈 You forecast energy expenses - 🧮 You need mathematical accuracy for financial planning @@ -212,27 +222,27 @@ When enabled, these entities override the corresponding Options Flow settings: #### Best Price Period Settings -| Entity | Type | Range | Description | -|--------|------|-------|-------------| -| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals | -| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average | -| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold | -| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Entity | Type | Range | Description | +| ------------------------------------- | ------ | ---------- | ------------------------------------------------------ | +| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals | +| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average | +| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | +| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | +| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | +| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold | +| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | #### Peak Price Period Settings -| Entity | Type | Range | Description | -|--------|------|-------|-------------| -| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | -| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | -| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | -| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Entity | Type | Range | Description | +| ------------------------------------- | ------ | ---------- | ------------------------------------------------------ | +| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | +| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | +| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | +| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | +| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | +| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | +| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | ### How Runtime Overrides Work @@ -257,24 +267,25 @@ Each configuration entity includes a detailed description attribute explaining w ```yaml automation: - - alias: "Winter: Stricter Best Price Detection" - trigger: - - platform: time - at: "00:00:00" - condition: - - condition: template - value_template: "{{ now().month in [11, 12, 1, 2] }}" - action: - - service: number.set_value - target: - entity_id: number._best_price_flexibility - data: - value: 10 # Stricter than default 15% + - alias: "Winter: Stricter Best Price Detection" + trigger: + - platform: time + at: "00:00:00" + condition: + - condition: template + value_template: "{{ now().month in [11, 12, 1, 2] }}" + action: + - service: number.set_value + target: + entity_id: number._best_price_flexibility + data: + value: 10 # Stricter than default 15% ``` ### Recorder Optimization (Optional) These configuration entities are designed to minimize database impact: + - **EntityCategory.CONFIG** - Excluded from Long-Term Statistics - All attributes excluded from history recording - Only state value changes are recorded @@ -285,16 +296,17 @@ However, if you prefer to **completely exclude** these entities from the recorde ```yaml recorder: - exclude: - entity_globs: - # Exclude all Tibber Prices configuration entities - - number.*_best_price_* - - number.*_peak_price_* - - switch.*_best_price_* - - switch.*_peak_price_* + exclude: + entity_globs: + # Exclude all Tibber Prices configuration entities + - number.*_best_price_* + - number.*_peak_price_* + - switch.*_best_price_* + - switch.*_peak_price_* ``` This is especially useful if: + - You rarely change these settings - You want the smallest possible database footprint - You don't need to see the history graph for these entities diff --git a/docs/user/versioned_docs/version-v0.29.0/dashboard-examples.md b/docs/user/versioned_docs/version-v0.29.0/dashboard-examples.md index 777b059..9fd206b 100644 --- a/docs/user/versioned_docs/version-v0.29.0/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.29.0/dashboard-examples.md @@ -12,13 +12,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor._current_electricity_price - name: Current Price - icon: mdi:flash - - entity: sensor._current_price_rating - name: Price Rating - - entity: sensor._next_electricity_price - name: Next Price + - entity: sensor._current_electricity_price + name: Current Price + icon: mdi:flash + - entity: sensor._current_price_rating + name: Price Rating + - entity: sensor._next_electricity_price + name: Next Price ``` ## Period Status Cards @@ -28,14 +28,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor._best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor._peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor._best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor._peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -48,16 +48,16 @@ entity: sensor._current_price_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -69,19 +69,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor._current_electricity_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor._current_electricity_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor._best_price_start - name: Best Period Starts - - entity: binary_sensor._best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor._best_price_start + name: Best Period Starts + - entity: binary_sensor._best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -93,25 +93,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor._current_electricity_price - - sensor._current_price_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor._current_electricity_price + - sensor._current_price_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor._price_today - - sensor._today_s_lowest_price - - sensor._today_s_highest_price + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor._price_today + - sensor._today_s_lowest_price + - sensor._today_s_highest_price ``` ## Icon Color Integration @@ -121,17 +121,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor._current_electricity_price - icon_color: "{{ state_attr('sensor._current_electricity_price', 'icon_color') }}" + - type: entity + entity: sensor._current_electricity_price + icon_color: "{{ state_attr('sensor._current_electricity_price', 'icon_color') }}" - - type: entity - entity: binary_sensor._best_price_period - icon_color: green + - type: entity + entity: binary_sensor._best_price_period + icon_color: green - - type: entity - entity: binary_sensor._peak_price_period - icon_color: red + - type: entity + entity: binary_sensor._peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -144,21 +144,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor._current_electricity_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor._current_electricity_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor._best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor._best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -168,21 +168,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor._*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor._*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.29.0/dynamic-icons.md b/docs/user/versioned_docs/version-v0.29.0/dynamic-icons.md index ed53f24..3be732d 100644 --- a/docs/user/versioned_docs/version-v0.29.0/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.29.0/dynamic-icons.md @@ -8,10 +8,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -26,10 +26,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_price_level`) -- Price rating sensors (e.g., `current_price_rating`) -- Volatility sensors (e.g., `today_s_price_volatility`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_price_level`) +- Price rating sensors (e.g., `current_price_rating`) +- Volatility sensors (e.g., `today_s_price_volatility`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -125,8 +125,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -134,24 +134,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor._best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -159,22 +159,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.29.0/faq.md b/docs/user/versioned_docs/version-v0.29.0/faq.md index 260b09d..7cf4220 100644 --- a/docs/user/versioned_docs/version-v0.29.0/faq.md +++ b/docs/user/versioned_docs/version-v0.29.0/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -118,18 +128,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor._best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor._best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -140,19 +150,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor._peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor._peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.29.0/glossary.md b/docs/user/versioned_docs/version-v0.29.0/glossary.md index ab5d9b5..e8c42d5 100644 --- a/docs/user/versioned_docs/version-v0.29.0/glossary.md +++ b/docs/user/versioned_docs/version-v0.29.0/glossary.md @@ -26,8 +26,9 @@ Quick reference for terms used throughout the documentation. **Config Entry ID** (also: `entry_id`) : A unique identifier assigned by Home Assistant to each configured integration instance. When this integration is used with multiple Tibber homes, each home gets its own Config Entry ID. All actions (`get_chartdata`, `get_apexcharts_yaml`, etc.) require this value as the `entry_id` parameter so that Home Assistant knows which home to query. - - **In the Action UI**: The field appears as a dropdown — select your home and HA fills in the ID automatically. - - **In YAML**: Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** menu, and choose **"Copy Config Entry ID"**. + +- **In the Action UI**: The field appears as a dropdown — select your home and HA fills in the ID automatically. +- **In YAML**: Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** menu, and choose **"Copy Config Entry ID"**. **Currency Display Mode** : Configurable setting for how prices are shown. Choose base currency (€, kr) or subunit (ct, øre). Smart defaults apply: EUR → subunit, NOK/SEK/DKK → base. @@ -114,6 +115,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.29.0/icon-colors.md b/docs/user/versioned_docs/version-v0.29.0/icon-colors.md index 8e1a868..0e17a00 100644 --- a/docs/user/versioned_docs/version-v0.29.0/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.29.0/icon-colors.md @@ -16,17 +16,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -40,12 +40,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_price_level`) -- Price rating sensors (e.g., `current_price_rating`) -- Volatility sensors (e.g., `today_s_price_volatility`) -- Price trend sensors (e.g., `price_trend_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_price_level`) +- Price rating sensors (e.g., `current_price_rating`) +- Volatility sensors (e.g., `today_s_price_volatility`) +- Price trend sensors (e.g., `price_trend_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -53,15 +53,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -297,12 +297,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -428,22 +428,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.29.0/installation.md b/docs/user/versioned_docs/version-v0.29.0/installation.md index 00cc47b..e745334 100644 --- a/docs/user/versioned_docs/version-v0.29.0/installation.md +++ b/docs/user/versioned_docs/version-v0.29.0/installation.md @@ -16,10 +16,10 @@ 2. Go to **Integrations** 3. Click the **⋮** menu (top right) → **Custom repositories** 4. Add the repository URL: - ``` - https://github.com/jpawlowski/hass.tibber_prices - ``` - Category: **Integration** + ``` + https://github.com/jpawlowski/hass.tibber_prices + ``` + Category: **Integration** 5. Click **Add** 6. Find **Tibber Price Information & Ratings** in the integration list 7. Click **Download** @@ -42,16 +42,16 @@ If you prefer not to use HACS: 1. Download the [latest release](https://github.com/jpawlowski/hass.tibber_prices/releases/latest) from GitHub 2. Extract the `custom_components/tibber_prices/` folder 3. Copy it to your Home Assistant `config/custom_components/` directory: - ``` - config/ - └── custom_components/ - └── tibber_prices/ - ├── __init__.py - ├── manifest.json - ├── sensor/ - ├── binary_sensor/ - └── ... - ``` + ``` + config/ + └── custom_components/ + └── tibber_prices/ + ├── __init__.py + ├── manifest.json + ├── sensor/ + ├── binary_sensor/ + └── ... + ``` 4. **Restart Home Assistant** 5. Continue with [Configuration](configuration.md) diff --git a/docs/user/versioned_docs/version-v0.29.0/intro.md b/docs/user/versioned_docs/version-v0.29.0/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.29.0/intro.md +++ b/docs/user/versioned_docs/version-v0.29.0/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.29.0/period-calculation.md b/docs/user/versioned_docs/version-v0.29.0/period-calculation.md index 696ea36..b5381cd 100644 --- a/docs/user/versioned_docs/version-v0.29.0/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.29.0/period-calculation.md @@ -6,19 +6,19 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [Fewer Periods Than Configured](#fewer-periods-than-configured) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Understanding Sensor Attributes](#understanding-sensor-attributes) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [Fewer Periods Than Configured](#fewer-periods-than-configured) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Understanding Sensor Attributes](#understanding-sensor-attributes) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) +- [Advanced Topics](#advanced-topics) --- @@ -28,8 +28,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -47,16 +47,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -157,7 +160,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -171,9 +174,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -213,8 +217,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -231,8 +235,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -247,8 +251,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -291,9 +295,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -327,7 +331,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -337,7 +341,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -376,11 +380,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -392,6 +398,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -439,10 +446,10 @@ Each attempt adds +3% flexibility and tries two filter combinations. The system ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -475,9 +482,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -508,7 +515,7 @@ This is **expected behavior** on days with very uniform electricity prices. When ```yaml min_periods_configured: 2 periods_found_total: 1 -flat_days_detected: 1 # Uniform prices today → 1 period is the right answer +flat_days_detected: 1 # Uniform prices today → 1 period is the right answer ``` You don't need to change anything. This is the integration protecting you from artificial periods. @@ -518,14 +525,16 @@ You don't need to change anything. This is the integration protecting you from a Relaxation tried all configured attempts but couldn't reach your target. Options: 1. **Increase relaxation attempts** (tries more flexibility levels before giving up) - ```yaml - relaxation_attempts_best: 12 # Default: 11 - ``` + + ```yaml + relaxation_attempts_best: 12 # Default: 11 + ``` 2. **Reduce minimum period count** - ```yaml - min_periods_best: 1 # Only require 1 period per day - ``` + + ```yaml + min_periods_best: 1 # Only require 1 period per day + ``` 3. **Check filter settings** – very strict `best_price_min_distance_from_avg` values block relaxation @@ -538,24 +547,26 @@ Relaxation tried all configured attempts but couldn't reach your target. Options **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -564,20 +575,22 @@ Relaxation tried all configured attempts but couldn't reach your target. Options **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -587,26 +600,26 @@ Relaxation tried all configured attempts but couldn't reach your target. Options # Entity: binary_sensor._best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Calculation summary (always shown – diagnostic overview of this calculation run): -min_periods_configured: 2 # What you configured as target -periods_found_total: 3 # What was actually found across all days +min_periods_configured: 2 # What you configured as target +periods_found_total: 3 # What was actually found across all days # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated -flat_days_detected: 1 # Days where prices were so flat that 1 period is enough -relaxation_incomplete: true # Some days couldn't reach the configured target +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +flat_days_detected: 1 # Days where prices were so flat that 1 period is enough +relaxation_incomplete: true # Some days couldn't reach the configured target ``` #### What the diagnostic attributes mean @@ -616,13 +629,13 @@ relaxation_incomplete: true # Some days couldn't reach the configured ta These two values together quickly show whether the calculation achieved its goal: ```yaml -min_periods_configured: 2 # You asked for 2 periods per day -periods_found_total: 6 # 3 days × 2 periods = fully satisfied ✅ +min_periods_configured: 2 # You asked for 2 periods per day +periods_found_total: 6 # 3 days × 2 periods = fully satisfied ✅ ``` ```yaml min_periods_configured: 2 -periods_found_total: 5 # 3 days, but one day got only 1 period +periods_found_total: 5 # 3 days, but one day got only 1 period ``` Note that `periods_found_total` counts **all periods across today and tomorrow** – so 4 on a two-day view means 2 per day on average. @@ -634,7 +647,7 @@ This is the most important diagnostic for days with very uniform prices (e.g. su ```yaml min_periods_configured: 2 periods_found_total: 1 -flat_days_detected: 1 # ← This explains why you got 1 instead of 2 +flat_days_detected: 1 # ← This explains why you got 1 instead of 2 ``` When prices barely change across the day – typically a variation of less than 10% – the integration automatically reduces the target from your configured value to 1. There is no meaningful second "best price window" when all prices are essentially equal. @@ -648,10 +661,11 @@ This flag appears when even after all relaxation attempts, at least one day coul ```yaml min_periods_configured: 2 periods_found_total: 1 -relaxation_incomplete: true # ← Relaxation tried everything, still short +relaxation_incomplete: true # ← Relaxation tried everything, still short ``` This is most common on very flat days (see above) or with very strict filter settings. If you see this repeatedly on normal days, consider: + - Reducing `min_periods_best` to 1 - Increasing `relaxation_attempts_best` - Checking if your `best_price_min_distance_from_avg` is too high @@ -665,12 +679,14 @@ This is most common on very flat days (see above) or with very strict filter set This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -696,6 +712,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -706,8 +723,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor._today_s_price_volatility: 8.2% # Low volatility -sensor._tomorrow_s_price_volatility: 7.9% # Also low +sensor._today_s_price_volatility: 8.2% # Low volatility +sensor._tomorrow_s_price_volatility: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -773,15 +791,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor._best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -792,8 +811,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -818,11 +837,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.29.0/sensors.md b/docs/user/versioned_docs/version-v0.29.0/sensors.md index 72e9086..aca9e04 100644 --- a/docs/user/versioned_docs/version-v0.29.0/sensors.md +++ b/docs/user/versioned_docs/version-v0.29.0/sensors.md @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -29,15 +29,15 @@ The integration provides several sensors that calculate average electricity pric #### Available Average Sensors -| Sensor | Description | Time Window | -|--------|-------------|-------------| -| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | -| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | -| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | -| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | -| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | -| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | -| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | +| Sensor | Description | Time Window | +| -------------------------- | -------------------------------------- | ------------------------------- | +| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | +| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | +| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | +| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | +| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | +| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | +| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | #### Configurable Display: Median vs Mean @@ -66,8 +66,8 @@ You can choose which value is displayed in the sensor state: 2. Click **Configure** on your home 3. Navigate to **Step 6: Average Sensor Display Settings** 4. Choose between: - - **Median** (default) - Shows typical price level, resistant to spikes - - **Arithmetic Mean** - Shows actual mathematical average + - **Median** (default) - Shows typical price level, resistant to spikes + - **Arithmetic Mean** - Shows actual mathematical average **Important:** Both values are **always available** as sensor attributes, regardless of your choice! This ensures your automations continue to work if you change the display setting. @@ -78,22 +78,22 @@ Both `price_mean` and `price_median` are always available as attributes: ```yaml # Example: Get both values regardless of display setting sensor: - - platform: template - sensors: - daily_price_analysis: - friendly_name: "Daily Price Analysis" - value_template: > - {% set median = state_attr('sensor._price_today', 'price_median') %} - {% set mean = state_attr('sensor._price_today', 'price_mean') %} - {% set current = states('sensor._current_electricity_price') | float %} + - platform: template + sensors: + daily_price_analysis: + friendly_name: "Daily Price Analysis" + value_template: > + {% set median = state_attr('sensor._price_today', 'price_median') %} + {% set mean = state_attr('sensor._price_today', 'price_mean') %} + {% set current = states('sensor._current_electricity_price') | float %} - {% if current < median %} - Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) - {% elif current < mean %} - Typical price range - {% else %} - Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) - {% endif %} + {% if current < median %} + Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) + {% elif current < mean %} + Typical price range + {% else %} + Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) + {% endif %} ``` #### Practical Examples @@ -104,21 +104,21 @@ Run dishwasher only when price is significantly below the daily typical level: ```yaml automation: - - alias: "Start Dishwasher When Cheap" - trigger: - - platform: state - entity_id: binary_sensor._best_price_period - to: "on" - condition: - # Only if current price is at least 20% below typical (median) - - condition: template - value_template: > - {% set current = states('sensor._current_electricity_price') | float %} - {% set median = state_attr('sensor._price_today', 'price_median') | float %} - {{ current < (median * 0.8) }} - action: - - service: switch.turn_on - entity_id: switch.dishwasher + - alias: "Start Dishwasher When Cheap" + trigger: + - platform: state + entity_id: binary_sensor._best_price_period + to: "on" + condition: + # Only if current price is at least 20% below typical (median) + - condition: template + value_template: > + {% set current = states('sensor._current_electricity_price') | float %} + {% set median = state_attr('sensor._price_today', 'price_median') | float %} + {{ current < (median * 0.8) }} + action: + - service: switch.turn_on + entity_id: switch.dishwasher ``` **Example 2: Cost-aware heating control** @@ -127,20 +127,20 @@ Use mean for actual cost calculations: ```yaml automation: - - alias: "Heating Budget Control" - trigger: - - platform: time - at: "06:00:00" - action: - # Calculate expected daily heating cost - - variables: - mean_price: "{{ state_attr('sensor._price_today', 'price_mean') | float }}" - heating_kwh_per_day: 15 # Estimated consumption - daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" - - service: notify.mobile_app - data: - title: "Heating Cost Estimate" - message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" + - alias: "Heating Budget Control" + trigger: + - platform: time + at: "06:00:00" + action: + # Calculate expected daily heating cost + - variables: + mean_price: "{{ state_attr('sensor._price_today', 'price_mean') | float }}" + heating_kwh_per_day: 15 # Estimated consumption + daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" + - service: notify.mobile_app + data: + title: "Heating Cost Estimate" + message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" ``` **Example 3: Smart charging based on rolling average** @@ -149,60 +149,61 @@ Use trailing average to understand recent price trends: ```yaml automation: - - alias: "EV Charging - Price Trend Based" - trigger: - - platform: state - entity_id: sensor.ev_battery_level - condition: - # Start charging if current price < 90% of recent 24h average - - condition: template - value_template: > - {% set current = states('sensor._current_electricity_price') | float %} - {% set trailing_avg = state_attr('sensor._price_trailing_24h', 'price_median') | float %} - {{ current < (trailing_avg * 0.9) }} - # And battery < 80% - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - action: - - service: switch.turn_on - entity_id: switch.ev_charger + - alias: "EV Charging - Price Trend Based" + trigger: + - platform: state + entity_id: sensor.ev_battery_level + condition: + # Start charging if current price < 90% of recent 24h average + - condition: template + value_template: > + {% set current = states('sensor._current_electricity_price') | float %} + {% set trailing_avg = state_attr('sensor._price_trailing_24h', 'price_median') | float %} + {{ current < (trailing_avg * 0.9) }} + # And battery < 80% + - condition: numeric_state + entity_id: sensor.ev_battery_level + below: 80 + action: + - service: switch.turn_on + entity_id: switch.ev_charger ``` #### Key Attributes All average sensors provide these attributes: -| Attribute | Description | Example | -|-----------|-------------|---------| -| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | -| `price_median` | Median value (always available) | 22.1 ct/kWh | -| `interval_count` | Number of intervals included | 96 | -| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | +| Attribute | Description | Example | +| ---------------- | ---------------------------------- | ------------------------- | +| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | +| `price_median` | Median value (always available) | 22.1 ct/kWh | +| `interval_count` | Number of intervals included | 96 | +| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | **Note:** The `price_mean` and `price_median` attributes are **always present** regardless of which value you configured for display. This ensures automation compatibility when changing the display setting. #### When to Use Which Value **Use Median for:** + - ✅ Comparing "typical" price levels across days - ✅ Determining if current price is unusually high/low - ✅ User-facing displays ("What was today like?") - ✅ Volatility analysis (comparing typical vs extremes) **Use Mean for:** + - ✅ Cost calculations and budgeting - ✅ Energy cost estimations - ✅ Comparing actual average costs between periods - ✅ Financial planning and forecasting **Both values tell different stories:** + - High median + much higher mean = Expensive spikes occurred - Low median + higher mean = Generally cheap with occasional spikes - Similar median and mean = Stable prices (low volatility) - - ## Volatility Sensors Volatility sensors help you understand how much electricity prices fluctuate over a given period. Instead of just looking at the absolute price, they measure the **relative price variation**, which is a great indicator of whether it's a good day for price-based energy optimization. @@ -217,21 +218,23 @@ The sensor's state can be `low`, `moderate`, `high`, or `very_high`, based on co ### Available Volatility Sensors -| Sensor | Description | Time Window | -|---|---|---| -| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | -| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | -| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | -| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | +| Sensor | Description | Time Window | +| ------------------------------------- | ----------------------------------------- | ---------------------- | +| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | +| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | +| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | +| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | ### Configuration You can adjust the CV thresholds that determine the volatility level: + 1. Go to **Settings → Devices & Services → Tibber Prices**. 2. Click **Configure**. 3. Go to the **Price Volatility Thresholds** step. Default thresholds are: + - **Moderate:** 15% - **High:** 30% - **Very High:** 50% @@ -240,14 +243,14 @@ Default thresholds are: All volatility sensors provide these attributes: -| Attribute | Description | Example | -|---|---|---| -| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` | -| `price_spread` | The difference between the highest and lowest price | `12.3` | -| `price_min` | The lowest price in the period | `10.2` | -| `price_max` | The highest price in the period | `22.5` | -| `price_mean` | The arithmetic mean of all prices in the period | `15.1` | -| `interval_count` | Number of price intervals included in the calculation | `96` | +| Attribute | Description | Example | +| ------------------------------- | ----------------------------------------------------- | ------- | +| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` | +| `price_spread` | The difference between the highest and lowest price | `12.3` | +| `price_min` | The lowest price in the period | `10.2` | +| `price_max` | The highest price in the period | `22.5` | +| `price_mean` | The arithmetic mean of all prices in the period | `15.1` | +| `interval_count` | Number of price intervals included in the calculation | `96` | ### Usage in Automations & Best Practices @@ -261,16 +264,17 @@ For automations, it is strongly recommended to use the `price_volatility` attrib **Good Example (Robust Automation):** This automation triggers only if the volatility is classified as `high` or `very_high`, respecting your central settings and working independently of the system language. + ```yaml automation: - - alias: "Enable battery optimization only on volatile days" - trigger: - - platform: template - value_template: > - {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }} - action: - - service: input_boolean.turn_on - entity_id: input_boolean.battery_optimization_enabled + - alias: "Enable battery optimization only on volatile days" + trigger: + - platform: template + value_template: > + {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }} + action: + - service: input_boolean.turn_on + entity_id: input_boolean.battery_optimization_enabled ``` --- @@ -283,20 +287,21 @@ You might be tempted to use the numeric `price_coefficient_variation_%` attribut **Bad Example (Brittle Automation):** This automation uses a hard-coded value. If you later change the "High" threshold in the integration's options to 35%, this automation will not respect that change and might trigger at the wrong time. + ```yaml automation: - - alias: "Brittle - Enable battery optimization" - trigger: - # - # BAD: Avoid hard-coding numeric values - # - - platform: numeric_state - entity_id: sensor._today_s_price_volatility - attribute: price_coefficient_variation_% - above: 30 - action: - - service: input_boolean.turn_on - entity_id: input_boolean.battery_optimization_enabled + - alias: "Brittle - Enable battery optimization" + trigger: + # + # BAD: Avoid hard-coding numeric values + # + - platform: numeric_state + entity_id: sensor._today_s_price_volatility + attribute: price_coefficient_variation_% + above: 30 + action: + - service: input_boolean.turn_on + entity_id: input_boolean.battery_optimization_enabled ``` By following the "Good Example", your automations become simpler, more readable, and much easier to maintain. @@ -315,11 +320,11 @@ difference = ((current_price - trailing_avg) / abs(trailing_avg)) × 100% This percentage is then classified: -| Rating | Condition (default) | Meaning | -|--------|---------------------|---------| -| **LOW** | difference ≤ -10% | Significantly below recent average | -| **NORMAL** | -10% < difference < +10% | Within normal range | -| **HIGH** | difference ≥ +10% | Significantly above recent average | +| Rating | Condition (default) | Meaning | +| ---------- | ------------------------ | ---------------------------------- | +| **LOW** | difference ≤ -10% | Significantly below recent average | +| **NORMAL** | -10% < difference < +10% | Within normal range | +| **HIGH** | difference ≥ +10% | Significantly above recent average | **Hysteresis** (default 2%) prevents flickering: once a rating enters LOW, it must cross -8% (not -10%) to return to NORMAL. This avoids rapid switching at threshold boundaries. @@ -341,29 +346,29 @@ stateDiagram-v2 ### Available Rating Sensors -| Sensor | Scope | Description | -|--------|-------|-------------| -| **Current Price Rating** | Current interval | Rating of the current 15-minute price | -| **Next Price Rating** | Next interval | Rating for the upcoming 15-minute price | -| **Previous Price Rating** | Previous interval | Rating for the past 15-minute price | +| Sensor | Scope | Description | +| ----------------------------- | ------------------ | --------------------------------------- | +| **Current Price Rating** | Current interval | Rating of the current 15-minute price | +| **Next Price Rating** | Next interval | Rating for the upcoming 15-minute price | +| **Previous Price Rating** | Previous interval | Rating for the past 15-minute price | | **Current Hour Price Rating** | Rolling 5-interval | Smoothed rating around the current hour | -| **Next Hour Price Rating** | Rolling 5-interval | Smoothed rating around the next hour | -| **Yesterday's Price Rating** | Calendar day | Aggregated rating for yesterday | -| **Today's Price Rating** | Calendar day | Aggregated rating for today | -| **Tomorrow's Price Rating** | Calendar day | Aggregated rating for tomorrow | +| **Next Hour Price Rating** | Rolling 5-interval | Smoothed rating around the next hour | +| **Yesterday's Price Rating** | Calendar day | Aggregated rating for yesterday | +| **Today's Price Rating** | Calendar day | Aggregated rating for today | +| **Tomorrow's Price Rating** | Calendar day | Aggregated rating for tomorrow | ### Ratings vs Levels The integration provides **two** classification systems that serve different purposes: -| | Ratings | Levels | -|--|---------|--------| -| **Source** | Calculated by integration | Provided by Tibber API | -| **Scale** | 3 levels (LOW, NORMAL, HIGH) | 5 levels (VERY_CHEAP → VERY_EXPENSIVE) | -| **Basis** | Trailing 24h average | Daily min/max range | -| **Best for** | Automations (simple thresholds) | Dashboard displays (fine granularity) | -| **Configurable** | Yes (thresholds) | Gap tolerance only | -| **Automation attribute** | `rating_level` (always lowercase English) | `level` (always uppercase English) | +| | Ratings | Levels | +| ------------------------ | ----------------------------------------- | -------------------------------------- | +| **Source** | Calculated by integration | Provided by Tibber API | +| **Scale** | 3 levels (LOW, NORMAL, HIGH) | 5 levels (VERY_CHEAP → VERY_EXPENSIVE) | +| **Basis** | Trailing 24h average | Daily min/max range | +| **Best for** | Automations (simple thresholds) | Dashboard displays (fine granularity) | +| **Configurable** | Yes (thresholds) | Gap tolerance only | +| **Automation attribute** | `rating_level` (always lowercase English) | `level` (always uppercase English) | **Which to use?** @@ -373,11 +378,11 @@ The integration provides **two** classification systems that serve different pur ### Key Attributes -| Attribute | Description | Example | -|-----------|-------------|---------| -| `rating_level` | Language-independent rating (always lowercase) | `low` | -| `difference` | Percentage difference from trailing average | `-12.5` | -| `trailing_avg_24h` | The reference average used for classification | `22.3` | +| Attribute | Description | Example | +| ------------------ | ---------------------------------------------- | ------- | +| `rating_level` | Language-independent rating (always lowercase) | `low` | +| `difference` | Percentage difference from trailing average | `-12.5` | +| `trailing_avg_24h` | The reference average used for classification | `22.3` | ### Usage in Automations @@ -411,26 +416,26 @@ See [Configuration](configuration.md#step-3-price-rating-thresholds) for details Level sensors show the **Tibber API's own price classification** with a 5-level scale: -| Level | Meaning | Numeric Value | -|-------|---------|---------------| -| **VERY_CHEAP** | Exceptionally low | -2 | -| **CHEAP** | Below average | -1 | -| **NORMAL** | Typical range | 0 | -| **EXPENSIVE** | Above average | +1 | -| **VERY_EXPENSIVE** | Exceptionally high | +2 | +| Level | Meaning | Numeric Value | +| ------------------ | ------------------ | ------------- | +| **VERY_CHEAP** | Exceptionally low | -2 | +| **CHEAP** | Below average | -1 | +| **NORMAL** | Typical range | 0 | +| **EXPENSIVE** | Above average | +1 | +| **VERY_EXPENSIVE** | Exceptionally high | +2 | ### Available Level Sensors -| Sensor | Scope | -|--------|-------| -| **Current Price Level** | Current interval | -| **Next Price Level** | Next interval | -| **Previous Price Level** | Previous interval | +| Sensor | Scope | +| ---------------------------- | ------------------------- | +| **Current Price Level** | Current interval | +| **Next Price Level** | Next interval | +| **Previous Price Level** | Previous interval | | **Current Hour Price Level** | Rolling 5-interval window | -| **Next Hour Price Level** | Rolling 5-interval window | -| **Yesterday's Price Level** | Calendar day (aggregated) | -| **Today's Price Level** | Calendar day (aggregated) | -| **Tomorrow's Price Level** | Calendar day (aggregated) | +| **Next Hour Price Level** | Rolling 5-interval window | +| **Yesterday's Price Level** | Calendar day (aggregated) | +| **Today's Price Level** | Calendar day (aggregated) | +| **Tomorrow's Price Level** | Calendar day (aggregated) | **Gap tolerance** smoothing is applied to prevent isolated level flickers (e.g., a single NORMAL between two CHEAPs → corrected to CHEAP). Configure in [options flow](configuration.md#step-4-price-level-gap-tolerance). @@ -440,31 +445,31 @@ These sensors show the lowest and highest prices for calendar days and rolling w ### Daily Min/Max -| Sensor | Description | -|--------|-------------| -| **Today's Lowest Price** | Minimum price today (00:00–23:59) | -| **Today's Highest Price** | Maximum price today (00:00–23:59) | -| **Tomorrow's Lowest Price** | Minimum price tomorrow | -| **Tomorrow's Highest Price** | Maximum price tomorrow | +| Sensor | Description | +| ---------------------------- | --------------------------------- | +| **Today's Lowest Price** | Minimum price today (00:00–23:59) | +| **Today's Highest Price** | Maximum price today (00:00–23:59) | +| **Tomorrow's Lowest Price** | Minimum price tomorrow | +| **Tomorrow's Highest Price** | Maximum price tomorrow | ### 24-Hour Rolling Min/Max -| Sensor | Description | -|--------|-------------| -| **Trailing Price Min** | Lowest price in the last 24 hours | +| Sensor | Description | +| ---------------------- | ---------------------------------- | +| **Trailing Price Min** | Lowest price in the last 24 hours | | **Trailing Price Max** | Highest price in the last 24 hours | -| **Leading Price Min** | Lowest price in the next 24 hours | -| **Leading Price Max** | Highest price in the next 24 hours | +| **Leading Price Min** | Lowest price in the next 24 hours | +| **Leading Price Max** | Highest price in the next 24 hours | ### Key Attributes All min/max sensors include: -| Attribute | Description | -|-----------|-------------| -| `timestamp` | When the extreme price occurs/occurred | -| `price_diff_from_daily_min` | Difference from daily minimum | -| `price_diff_from_daily_min_%` | Percentage difference | +| Attribute | Description | +| ----------------------------- | -------------------------------------- | +| `timestamp` | When the extreme price occurs/occurred | +| `price_diff_from_daily_min` | Difference from daily minimum | +| `price_diff_from_daily_min_%` | Percentage difference | ## Timing Sensors @@ -490,14 +495,14 @@ stateDiagram-v2 For each period type (Best Price and Peak Price): -| Sensor | When Period Active | When No Active Period | -|--------|-------------------|----------------------| -| **End Time** | Current period's end time | Next period's end time | -| **Period Duration** | Current period length (minutes) | Next period length | -| **Remaining Minutes** | Minutes until current period ends | 0 | -| **Progress** | 0–100% through current period | 0 | -| **Next Start Time** | When next-next period starts | When next period starts | -| **Next In Minutes** | Minutes to next-next period | Minutes to next period | +| Sensor | When Period Active | When No Active Period | +| --------------------- | --------------------------------- | ----------------------- | +| **End Time** | Current period's end time | Next period's end time | +| **Period Duration** | Current period length (minutes) | Next period length | +| **Remaining Minutes** | Minutes until current period ends | 0 | +| **Progress** | 0–100% through current period | 0 | +| **Next Start Time** | When next-next period starts | When next period starts | +| **Next In Minutes** | Minutes to next-next period | Minutes to next period | ### Usage Examples @@ -560,25 +565,26 @@ The integration provides two families of trend sensors for different use cases: These sensors compare the **current price** with the **average price** of the next N hours: -| Sensor | Compares Against | -|--------|-----------------| -| **Price Trend (1h)** | Average of next 1 hour | -| **Price Trend (2h)** | Average of next 2 hours | -| **Price Trend (3h)** | Average of next 3 hours | -| **Price Trend (4h)** | Average of next 4 hours | -| **Price Trend (5h)** | Average of next 5 hours | -| **Price Trend (6h)** | Average of next 6 hours | -| **Price Trend (8h)** | Average of next 8 hours | +| Sensor | Compares Against | +| --------------------- | ------------------------ | +| **Price Trend (1h)** | Average of next 1 hour | +| **Price Trend (2h)** | Average of next 2 hours | +| **Price Trend (3h)** | Average of next 3 hours | +| **Price Trend (4h)** | Average of next 4 hours | +| **Price Trend (5h)** | Average of next 5 hours | +| **Price Trend (6h)** | Average of next 6 hours | +| **Price Trend (8h)** | Average of next 8 hours | | **Price Trend (12h)** | Average of next 12 hours | :::info Same Starting Point — All Sensors Use Your Current Price All trend sensors share the **same base: your current 15-minute price**. They differ only in how far ahead they average. The windows **overlap** — the 3h average includes ALL intervals from the 1h and 2h windows, plus one more hour. **This means:** + - `price_trend_3h` shows "current price vs. average of the **entire** next 3 hours" — **not** "what happens between hour 2 and hour 3" - If 1h shows `falling` but 6h shows `rising`: near-term prices are below your current price, but looking at the full 6h window (which includes expensive evening hours), the overall average is above your current price - Larger windows smooth out short-term fluctuations — a 30-minute price spike affects the 1h average more than the 6h average -::: + ::: **States:** Each sensor has one of five states: @@ -602,27 +608,27 @@ stateDiagram-v2 F --> SF: accelerates ``` -| State | Meaning | `trend_value` | -|-------|---------|---------------| -| `strongly_falling` | Prices will drop significantly | -2 | -| `falling` | Prices will drop | -1 | -| `stable` | Prices staying roughly the same | 0 | -| `rising` | Prices will increase | +1 | -| `strongly_rising` | Prices will increase significantly | +2 | +| State | Meaning | `trend_value` | +| ------------------ | ---------------------------------- | ------------- | +| `strongly_falling` | Prices will drop significantly | -2 | +| `falling` | Prices will drop | -1 | +| `stable` | Prices staying roughly the same | 0 | +| `rising` | Prices will increase | +1 | +| `strongly_rising` | Prices will increase significantly | +2 | **Key attributes:** -| Attribute | Description | Example | -|-----------|-------------|---------| -| `trend_value` | Numeric value for automations (-2 to +2) | `-1` | -| `trend_Nh_%` | Percentage difference from current price | `-12.3` | -| `next_Nh_avg` | Average price in the future window | `18.5` | -| `second_half_Nh_avg` | Average price in later half of window | `16.2` | -| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` | -| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` | -| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` | -| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` | -| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` | +| Attribute | Description | Example | +| ------------------------------ | --------------------------------------------------------------------- | ------- | +| `trend_value` | Numeric value for automations (-2 to +2) | `-1` | +| `trend_Nh_%` | Percentage difference from current price | `-12.3` | +| `next_Nh_avg` | Average price in the future window | `18.5` | +| `second_half_Nh_avg` | Average price in later half of window | `16.2` | +| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` | +| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` | +| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` | +| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` | +| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` | **Tip:** The `trend_value` attribute (`-2` to `+2`) is ideal for automations — use numeric comparisons instead of matching translated state strings. @@ -638,11 +644,11 @@ Unlike the simple trend sensors that always compare current price vs future aver **Key attributes:** -| Attribute | Description | Example | -|-----------|-------------|---------| -| `previous_direction` | Price direction before the current trend started | `falling` | -| `price_direction_duration_minutes` | How long prices have been moving in this direction | `45` | -| `price_direction_since` | Timestamp when prices started moving in this direction | `2025-11-08T14:00:00+01:00` | +| Attribute | Description | Example | +| ---------------------------------- | ------------------------------------------------------ | --------------------------- | +| `previous_direction` | Price direction before the current trend started | `falling` | +| `price_direction_duration_minutes` | How long prices have been moving in this direction | `45` | +| `price_direction_since` | Timestamp when prices started moving in this direction | `2025-11-08T14:00:00+01:00` | ### Next Price Trend Change @@ -652,11 +658,11 @@ This sensor predicts **when the current trend will change** by scanning future i **Important:** Only **direction changes** count as trend changes. The five states are grouped into three directions: -| Direction | States | -|-----------|--------| +| Direction | States | +| ----------- | ----------------------------- | | **falling** | `strongly_falling`, `falling` | -| **stable** | `stable` | -| **rising** | `rising`, `strongly_rising` | +| **stable** | `stable` | +| **rising** | `rising`, `strongly_rising` | A change from `rising` to `strongly_rising` (same direction) is **not** reported as a trend change — only actual reversals like `rising` → `stable` or `falling` → `rising`. @@ -664,18 +670,18 @@ A change from `rising` to `strongly_rising` (same direction) is **not** reported **Key attributes:** -| Attribute | Description | Example | -|-----------|-------------|---------| -| `direction` | What the trend will change TO | `rising` | -| `from_direction` | Current trend (will change FROM) | `falling` | -| `minutes_until_change` | Minutes until trend changes | `90` | -| `price_at_change` | Price at the change point | `13.8` | -| `price_avg_after_change` | Average price after change | `18.1` | -| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` | -| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` | -| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` | -| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` | -| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` | +| Attribute | Description | Example | +| ------------------------------ | --------------------------------------------------------------------- | --------- | +| `direction` | What the trend will change TO | `rising` | +| `from_direction` | Current trend (will change FROM) | `falling` | +| `minutes_until_change` | Minutes until trend changes | `90` | +| `price_at_change` | Price at the change point | `13.8` | +| `price_avg_after_change` | Average price after change | `18.1` | +| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` | +| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` | +| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` | +| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` | +| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` | ### How to Use Trend Sensors for Decisions @@ -688,13 +694,13 @@ A natural intuition is to treat trend states like a stock ticker: **This is wrong.** Trend sensors don't show a trajectory — they show a **comparison** between your current price and future prices. The correct interpretation is the opposite: -| State | What the Sensor Calculates | ✅ Correct Action | -|-------|---------------------------|-------------------| -| `falling` | Current price **higher** than future average | **WAIT** — cheaper prices are coming | -| `strongly_falling` | Current price **much higher** than future average | **DEFINITELY WAIT** — significant savings ahead | -| `stable` | Current price **≈ equal** to future average | **Timing doesn't matter** — start whenever convenient | -| `rising` | Current price **lower** than future average | **ACT NOW** — it only gets more expensive | -| `strongly_rising` | Current price **much lower** than future average | **ACT IMMEDIATELY** — best price right now | +| State | What the Sensor Calculates | ✅ Correct Action | +| ------------------ | ------------------------------------------------- | ----------------------------------------------------- | +| `falling` | Current price **higher** than future average | **WAIT** — cheaper prices are coming | +| `strongly_falling` | Current price **much higher** than future average | **DEFINITELY WAIT** — significant savings ahead | +| `stable` | Current price **≈ equal** to future average | **Timing doesn't matter** — start whenever convenient | +| `rising` | Current price **lower** than future average | **ACT NOW** — it only gets more expensive | +| `strongly_rising` | Current price **much lower** than future average | **ACT IMMEDIATELY** — best price right now | **"Rising" is NOT "too late" — it means NOW is the best time because prices will be higher later.** ::: @@ -706,31 +712,31 @@ For most appliances (dishwasher, washing machine, dryer), a single trend sensor ```yaml # Example: Start dishwasher when prices are favorable trigger: - - platform: state - entity_id: sensor.my_home_price_trend_3h + - platform: state + entity_id: sensor.my_home_price_trend_3h condition: - - condition: numeric_state - entity_id: sensor.my_home_price_trend_3h - attribute: trend_value - # rising (1) or strongly_rising (2) = act now - above: 0 + - condition: numeric_state + entity_id: sensor.my_home_price_trend_3h + attribute: trend_value + # rising (1) or strongly_rising (2) = act now + above: 0 action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` #### Combining Multiple Windows When short-term and long-term trends disagree, you get richer insight: -| 1h Trend | 6h Trend | Interpretation | Recommendation | -|----------|----------|---------------|----------------| -| `rising` | `rising` | Prices going up across the board | **Start now** | -| `falling` | `falling` | Prices dropping across the board | **Wait** | -| `falling` | `rising` | Brief dip, then expensive evening | **Wait briefly**, then start during the dip | -| `rising` | `falling` | Short spike, but cheaper hours ahead | **Wait** if you can — better prices coming | -| `stable` | any | Short-term doesn't matter | Use the **longer window** for your decision | +| 1h Trend | 6h Trend | Interpretation | Recommendation | +| --------- | --------- | ------------------------------------ | ------------------------------------------- | +| `rising` | `rising` | Prices going up across the board | **Start now** | +| `falling` | `falling` | Prices dropping across the board | **Wait** | +| `falling` | `rising` | Brief dip, then expensive evening | **Wait briefly**, then start during the dip | +| `rising` | `falling` | Short spike, but cheaper hours ahead | **Wait** if you can — better prices coming | +| `stable` | any | Short-term doesn't matter | Use the **longer window** for your decision | #### Dashboard Quick-Glance @@ -744,12 +750,12 @@ On your dashboard, trend sensors give an instant overview: Both sensor families provide future price information, but serve different purposes: -| | Trend Sensors | Average Sensors | -|--|---------------|-----------------| -| **Purpose** | Dashboard display, quick visual overview | Automations, precise numeric comparisons | -| **Output** | Classification (falling/stable/rising) | Exact price values (ct/kWh) | -| **Best for** | "Should I worry about prices?" | "Is the future average below 15 ct?" | -| **Use in** | Dashboard icons, status displays | Template conditions, numeric thresholds | +| | Trend Sensors | Average Sensors | +| ------------ | ---------------------------------------- | ---------------------------------------- | +| **Purpose** | Dashboard display, quick visual overview | Automations, precise numeric comparisons | +| **Output** | Classification (falling/stable/rising) | Exact price values (ct/kWh) | +| **Best for** | "Should I worry about prices?" | "Is the future average below 15 ct?" | +| **Use in** | Dashboard icons, status displays | Template conditions, numeric thresholds | **Design principle:** Use **trend sensors** (enum) for visual feedback at a glance, use **average sensors** (numeric) for precise decision-making in automations. @@ -776,19 +782,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -809,24 +815,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_docs/version-v0.30.0/actions.md b/docs/user/versioned_docs/version-v0.30.0/actions.md index c94b937..383d6af 100644 --- a/docs/user/versioned_docs/version-v0.30.0/actions.md +++ b/docs/user/versioned_docs/version-v0.30.0/actions.md @@ -38,13 +38,13 @@ If you have configured more than one Tibber home, each home has its own entry ID **Key Features:** -- **Flexible Output Formats**: Array of objects or array of arrays -- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) -- **Price Filtering**: Filter by price level or rating -- **Period Support**: Return best/peak price period summaries instead of intervals -- **Resolution Control**: Interval (15-minute) or hourly aggregation -- **Customizable Field Names**: Rename output fields to match your chart library -- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) +- **Flexible Output Formats**: Array of objects or array of arrays +- **Time Range Selection**: Filter by day (yesterday, today, tomorrow) +- **Price Filtering**: Filter by price level or rating +- **Period Support**: Return best/peak price period summaries instead of intervals +- **Resolution Control**: Interval (15-minute) or hourly aggregation +- **Customizable Field Names**: Rename output fields to match your chart library +- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh) **Basic Example:** @@ -76,14 +76,14 @@ response_variable: chart_data **Common Parameters:** -| Parameter | Description | Default | -| ---------------- | ------------------------------------------- | ----------------------- | -| `entry_id` | Integration entry ID (required) | - | -| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | -| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | -| `resolution` | `interval` (15-min) or `hourly` | `interval` | -| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | -| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | +| Parameter | Description | Default | +| ------------------ | --------------------------------------------------------------------------- | ----------------------- | +| `entry_id` | Integration entry ID (required) | - | +| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` | +| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` | +| `resolution` | `interval` (15-min) or `hourly` | `interval` | +| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting | +| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) | **Rolling Window Mode:** @@ -99,8 +99,9 @@ response_variable: chart_data ``` **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow -- **When tomorrow data not available**: Returns yesterday + today + +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today This is useful for charts that should always show a 48-hour window without manual day selection. @@ -138,12 +139,12 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U You can include the raw energy price (spot price) and/or tax component in chart data output. This is useful for visualizing how the total price is composed over time, or for feed-in calculations. -| Parameter | Description | Default | -|-----------|-------------|---------| -| `include_energy` | Include raw energy/spot price per data point | `false` | -| `include_tax` | Include tax/fees component per data point | `false` | -| `energy_field` | Custom field name for energy price | `energy_price` | -| `tax_field` | Custom field name for tax | `tax` | +| Parameter | Description | Default | +| ---------------- | -------------------------------------------- | -------------- | +| `include_energy` | Include raw energy/spot price per data point | `false` | +| `include_tax` | Include tax/fees component per data point | `false` | +| `energy_field` | Custom field name for energy price | `energy_price` | +| `tax_field` | Custom field name for tax | `tax` | **Example: Chart with price composition** @@ -185,6 +186,7 @@ Returns data points like: **Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level. **Prerequisites:** + - [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) - [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling) @@ -202,9 +204,9 @@ Returns data points like: service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID - day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom - level_type: rating_level # or "level" for 5-level classification - highlight_best_price: true # Show best price period overlays + day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom + level_type: rating_level # or "level" for 5-level classification + highlight_best_price: true # Show best price period overlays response_variable: apexcharts_config ``` @@ -212,9 +214,9 @@ response_variable: apexcharts_config - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies - **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor - **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes) - - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor + - **✨ Includes dynamic Y-axis scaling** via `chart_metadata` sensor **Dynamic Y-Axis Scaling (Rolling Window Modes):** @@ -247,7 +249,7 @@ service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID # Omit 'day' for rolling window (or use 'rolling_window') - level_type: level # 5-level classification + level_type: level # 5-level classification highlight_best_price: true response_variable: config @@ -255,7 +257,7 @@ response_variable: config type: custom:config-template-card entities: - binary_sensor._tomorrow_s_data_available - - sensor._chart_metadata # For dynamic Y-axis + - sensor._chart_metadata # For dynamic Y-axis card: # ... paste generated config ``` @@ -272,6 +274,7 @@ _Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolli **Best Price Period Highlights:** When `highlight_best_price: true`: + - Vertical bands overlay the chart showing detected best price periods - Tooltip shows "Best Price Period" label when hovering over highlighted areas - Only appears when best price periods are configured and detected @@ -311,10 +314,10 @@ If you're still using the `sensor._chart_data_export` sensor, conside **Benefits:** -- No HA restart required for configuration changes -- More flexible filtering and formatting options -- Better performance (on-demand instead of polling) -- Future-proof (active development) +- No HA restart required for configuration changes +- More flexible filtering and formatting options +- Better performance (on-demand instead of polling) +- Future-proof (active development) **Migration Steps:** diff --git a/docs/user/versioned_docs/version-v0.30.0/automation-examples.md b/docs/user/versioned_docs/version-v0.30.0/automation-examples.md index 7f47587..252e56a 100644 --- a/docs/user/versioned_docs/version-v0.30.0/automation-examples.md +++ b/docs/user/versioned_docs/version-v0.30.0/automation-examples.md @@ -4,16 +4,17 @@ ## Table of Contents -- [Price-Based Automations](#price-based-automations) -- [Volatility-Aware Automations](#volatility-aware-automations) -- [Best Hour Detection](#best-hour-detection) -- [ApexCharts Cards](#apexcharts-cards) +- [Price-Based Automations](#price-based-automations) +- [Volatility-Aware Automations](#volatility-aware-automations) +- [Best Hour Detection](#best-hour-detection) +- [ApexCharts Cards](#apexcharts-cards) --- > **Important Note:** The following examples are intended as templates to illustrate the logic. They are **not** suitable for direct copy & paste without adaptation. > > Please make sure you: +> > 1. Replace the **Entity IDs** (e.g., `sensor._...`, `switch.pool_pump`) with the IDs of your own devices and sensors. > 2. Adapt the logic to your specific devices (e.g., heat pump, EV, water boiler). > @@ -254,14 +255,14 @@ A common misconception: **"rising" does NOT mean "too late"**. It means your cur ### Sensor Combination Quick Reference -| What You Want | Sensors to Combine | -|---|---| -| **"Is it cheap right now?"** | `rating_level` attribute (VERY_CHEAP, CHEAP) | -| **"Will prices go up or down?"** | `current_price_trend` state (falling/stable/rising) | -| **"When will the trend change?"** | `next_price_trend_change` state (timestamp) | -| **"How cheap will it get?"** | `next_Nh_avg` attribute on trend sensors | -| **"Is the price drop meaningful?"** | `today_s_price_volatility` (not low = meaningful) | -| **"Ride the full cheap wave"** | `rating_level` + `current_price_trend` + `best_price_period` | +| What You Want | Sensors to Combine | +| ----------------------------------- | ------------------------------------------------------------ | +| **"Is it cheap right now?"** | `rating_level` attribute (VERY_CHEAP, CHEAP) | +| **"Will prices go up or down?"** | `current_price_trend` state (falling/stable/rising) | +| **"When will the trend change?"** | `next_price_trend_change` state (timestamp) | +| **"How cheap will it get?"** | `next_Nh_avg` attribute on trend sensors | +| **"Is the price drop meaningful?"** | `today_s_price_volatility` (not low = meaningful) | +| **"Ride the full cheap wave"** | `rating_level` + `current_price_trend` + `best_price_period` | --- @@ -302,16 +303,15 @@ automation: - service: notify.mobile_app data: message: > - Home battery charging started. Price: {{ states('sensor._current_electricity_price') }} {{ state_attr('sensor._current_electricity_price', 'unit_of_measurement') }}. - Today's volatility is {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') }}. - + Home battery charging started. Price: {{ states('sensor._current_electricity_price') }} {{ state_attr('sensor._current_electricity_price', 'unit_of_measurement') }}. + Today's volatility is {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') }}. ``` **Why this works:** -- The automation only runs if volatility is `moderate`, `high`, or `very_high`. -- If you adjust your volatility thresholds in the future, this automation adapts automatically without any changes. -- It uses the `price_volatility` attribute, ensuring it works correctly regardless of your Home Assistant's display language. +- The automation only runs if volatility is `moderate`, `high`, or `very_high`. +- If you adjust your volatility thresholds in the future, this automation adapts automatically without any changes. +- It uses the `price_volatility` attribute, ensuring it works correctly regardless of your Home Assistant's display language. ### Use Case: Combined Volatility and Absolute Price Check @@ -354,9 +354,9 @@ automation: **Why this works:** -- On days with meaningful price swings, it charges during any `Best Price` period. -- On days with flat prices, it still charges if the price drops below your personal "cheap enough" threshold (e.g., 0.18 €/kWh or 18 ct/kWh). -- This gracefully handles midnight period flips, as the absolute price check will likely remain true if prices stay low. +- On days with meaningful price swings, it charges during any `Best Price` period. +- On days with flat prices, it still charges if the price drops below your personal "cheap enough" threshold (e.g., 0.18 €/kWh or 18 ct/kWh). +- This gracefully handles midnight period flips, as the absolute price check will likely remain true if prices stay low. ### Use Case: Using the Period's Own Volatility Attribute @@ -385,10 +385,10 @@ automation: **Why this works:** -- Each detected period has its own `volatility` attribute (`low`, `moderate`, etc.). -- This is the simplest way to check for meaningful savings for that specific period. -- The attribute name on the binary sensor is `volatility` (lowercase) and its value is also lowercase. -- It also contains other useful attributes like `price_mean`, `price_spread`, and the `price_coefficient_variation_%` for that period. +- Each detected period has its own `volatility` attribute (`low`, `moderate`, etc.). +- This is the simplest way to check for meaningful savings for that specific period. +- The attribute name on the binary sensor is `volatility` (lowercase) and its value is also lowercase. +- It also contains other useful attributes like `price_mean`, `price_spread`, and the `price_coefficient_variation_%` for that period. --- @@ -414,9 +414,9 @@ automation: action: # Compare different future windows to find cheapest start - variables: - next_2h: "{{ state_attr('sensor._price_outlook_2h', 'next_2h_avg') | float(999) }}" - next_4h: "{{ state_attr('sensor._price_outlook_4h', 'next_4h_avg') | float(999) }}" - daily_avg: "{{ state_attr('sensor._price_today', 'price_median') | float(999) }}" + next_2h: "{{ state_attr('sensor._price_outlook_2h', 'next_2h_avg') | float(999) }}" + next_4h: "{{ state_attr('sensor._price_outlook_4h', 'next_4h_avg') | float(999) }}" + daily_avg: "{{ state_attr('sensor._price_today', 'price_median') | float(999) }}" - service: notify.mobile_app data: title: "Dishwasher Scheduling" @@ -479,11 +479,11 @@ The examples below contain `entry_id: YOUR_ENTRY_ID`. This value identifies whic **Required:** -- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS +- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS **Optional (for rolling window mode):** -- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS +- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS ### Installation @@ -520,9 +520,9 @@ response_variable: apexcharts_config **Behavior:** -- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow -- **When tomorrow data not available**: Shows yesterday + today -- **Fixed 48h span:** Always shows full 48 hours +- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow +- **When tomorrow data not available**: Shows yesterday + today +- **Fixed 48h span:** Always shows full 48 hours **Auto-Zoom Variant:** @@ -537,17 +537,17 @@ data: response_variable: apexcharts_config ``` -- Same data loading as rolling window -- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight -- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight +- Same data loading as rolling window +- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight +- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight **Note:** Rolling window modes require Config Template Card to dynamically adjust the time range. ### Features -- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) -- Best price period highlighting (semi-transparent green overlay) -- Automatic NULL insertion for clean gaps -- Translated labels based on your Home Assistant language -- Interactive zoom and pan -- Live marker showing current time +- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) +- Best price period highlighting (semi-transparent green overlay) +- Automatic NULL insertion for clean gaps +- Translated labels based on your Home Assistant language +- Interactive zoom and pan +- Live marker showing current time diff --git a/docs/user/versioned_docs/version-v0.30.0/chart-examples.md b/docs/user/versioned_docs/version-v0.30.0/chart-examples.md index 2648378..635fe14 100644 --- a/docs/user/versioned_docs/version-v0.30.0/chart-examples.md +++ b/docs/user/versioned_docs/version-v0.30.0/chart-examples.md @@ -18,14 +18,15 @@ Every example below contains `entry_id: YOUR_ENTRY_ID`. This value identifies wh The integration can generate 4 different chart modes, each optimized for specific use cases: -| Mode | Description | Best For | Dependencies | -|------|-------------|----------|--------------| -| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | -| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | -| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | -| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | +| Mode | Description | Best For | Dependencies | +| ---------------------------- | ---------------------------------------------------- | -------------------------------- | --------------------------------- | +| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card | +| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card | +| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card | +| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card | **Screenshots available for:** + - ✅ Today (static) - Representative of all fixed day views - ✅ Rolling Window - Shows dynamic Y-axis scaling - ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect @@ -39,6 +40,7 @@ The integration can generate 4 different chart modes, each optimized for specifi **Dependencies:** ApexCharts Card only **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -53,6 +55,7 @@ data: ![Today's Prices - Static 24h View](/img/charts/today.jpg) **Key Features:** + - ✅ Color-coded price levels (LOW, NORMAL, HIGH) - ✅ Best price period highlights (vertical bands) - ✅ Static 24-hour view (00:00 - 23:59) @@ -69,6 +72,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -83,6 +87,7 @@ data: ![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg) **Key Features:** + - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today - ✅ Always shows 48 hours of data @@ -90,6 +95,7 @@ data: - ✅ Color gradients for visual appeal **How it works:** + - Before ~13:00: Shows yesterday + today - After ~13:00: Shows today + tomorrow - Y-axis automatically adjusts to data range for optimal visualization @@ -103,6 +109,7 @@ data: **Dependencies:** ApexCharts Card + Config Template Card **Generate:** + ```yaml service: tibber_prices.get_apexcharts_yaml data: @@ -117,6 +124,7 @@ data: ![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg) **Key Features:** + - ✅ **Progressive zoom:** Graph span decreases every 15 minutes - ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor - ✅ Always shows: 2 hours lookback + remaining time until midnight @@ -124,6 +132,7 @@ data: - ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window) **How it works:** + - 00:00: Shows full 48h window (same as rolling window) - 06:00: Shows 04:00 → midnight (20h window) - 12:00: Shows 10:00 → midnight (14h window) @@ -165,6 +174,7 @@ Based on **absolute price ranges** (calculated from daily min/max): Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization: **Without chart_metadata sensor (disabled):** + ``` ┌─────────────────────┐ │ │ ← Lots of empty space @@ -176,6 +186,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s ``` **With chart_metadata sensor (enabled):** + ``` ┌─────────────────────┐ │ ___ │ ← Y-axis fitted to data @@ -201,6 +212,7 @@ Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` s When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods: **Example:** + ``` Price │ @@ -215,6 +227,7 @@ Price ``` **Features:** + - Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md)) - Tooltip shows "Best Price Period" label - Only appears when periods are configured and detected @@ -227,18 +240,18 @@ Price ### Required for All Modes - **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library - ```bash - # Install via HACS - HACS → Frontend → Search "ApexCharts Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "ApexCharts Card" → Download + ``` ### Required for Rolling Window Modes Only - **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration - ```bash - # Install via HACS - HACS → Frontend → Search "Config Template Card" → Download - ``` + ```bash + # Install via HACS + HACS → Frontend → Search "Config Template Card" → Download + ``` **Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone! @@ -253,9 +266,9 @@ Edit the `colors` array in the generated YAML: ```yaml apex_config: colors: - - "#00FF00" # Change LOW/VERY_CHEAP color - - "#0000FF" # Change NORMAL color - - "#FF0000" # Change HIGH/VERY_EXPENSIVE color + - "#00FF00" # Change LOW/VERY_CHEAP color + - "#0000FF" # Change NORMAL color + - "#FF0000" # Change HIGH/VERY_EXPENSIVE color ``` ### Changing Chart Height @@ -270,7 +283,7 @@ header: title: My Custom Title apex_config: chart: - height: 400 # Adjust height in pixels + height: 400 # Adjust height in pixels ``` ### Combining with Other Cards @@ -302,14 +315,14 @@ cards: 1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow) - ![Today View](/img/charts/today.jpg) + ![Today View](/img/charts/today.jpg) 2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window - ![Rolling Window](/img/charts/rolling-window.jpg) + ![Rolling Window](/img/charts/rolling-window.jpg) 3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect - ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) + ![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg) **Note:** Tomorrow view is visually identical to Today view (same chart type, just different data). diff --git a/docs/user/versioned_docs/version-v0.30.0/concepts.md b/docs/user/versioned_docs/version-v0.30.0/concepts.md index f0befd2..e650261 100644 --- a/docs/user/versioned_docs/version-v0.30.0/concepts.md +++ b/docs/user/versioned_docs/version-v0.30.0/concepts.md @@ -57,6 +57,7 @@ Prices are automatically classified into **rating levels**: - **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads) Rating is based on **statistical analysis** comparing current price to: + - Daily average - Trailing 24-hour average - User-configured thresholds @@ -69,6 +70,7 @@ Rating is based on **statistical analysis** comparing current price to: - **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption) Periods can: + - Span multiple hours - Cross midnight boundaries - Adapt based on your configuration (flex, min_distance, rating levels) @@ -100,6 +102,7 @@ See [Automation Examples → V-Shaped Days](automation-examples.md#understanding ## Multi-Home Support You can add multiple Tibber homes to track prices for: + - Different locations - Different electricity contracts - Comparison between regions @@ -109,6 +112,7 @@ Each home gets its own set of sensors with unique entity IDs. --- 💡 **Next Steps:** + - [Glossary](glossary.md) - Detailed term definitions - [Sensors](sensors.md) - How to use sensor data - [Automation Examples](automation-examples.md) - Practical use cases diff --git a/docs/user/versioned_docs/version-v0.30.0/configuration.md b/docs/user/versioned_docs/version-v0.30.0/configuration.md index 0ed5d9d..e51d855 100644 --- a/docs/user/versioned_docs/version-v0.30.0/configuration.md +++ b/docs/user/versioned_docs/version-v0.30.0/configuration.md @@ -63,12 +63,12 @@ All steps have sensible defaults — you can click through without changes and f Configure how the integration classifies prices relative to the 24-hour trailing average: -| Setting | Default | Description | -|---------|---------|-------------| -| **Low threshold** | -10% | Prices this much below average → **LOW** rating | -| **High threshold** | +10% | Prices this much above average → **HIGH** rating | -| **Hysteresis** | 2% | Prevents flickering at threshold boundaries | -| **Gap tolerance** | 1 | Smooth isolated rating blocks (e.g., lone NORMAL between two LOWs) | +| Setting | Default | Description | +| ------------------ | ------- | ------------------------------------------------------------------ | +| **Low threshold** | -10% | Prices this much below average → **LOW** rating | +| **High threshold** | +10% | Prices this much above average → **HIGH** rating | +| **Hysteresis** | 2% | Prevents flickering at threshold boundaries | +| **Gap tolerance** | 1 | Smooth isolated rating blocks (e.g., lone NORMAL between two LOWs) | ### Step 4: Price Level Gap Tolerance @@ -80,26 +80,29 @@ Configure how the integration classifies prices relative to the 24-hour trailing Configure the Coefficient of Variation (CV) boundaries: -| Level | Default | Meaning | -|-------|---------|---------| -| **Moderate** | 15% | Noticeable price variation, some optimization potential | -| **High** | 30% | Significant price swings, good for timing optimization | -| **Very High** | 50% | Extreme volatility, maximum optimization benefit | +| Level | Default | Meaning | +| ------------- | ------- | ------------------------------------------------------- | +| **Moderate** | 15% | Noticeable price variation, some optimization potential | +| **High** | 30% | Significant price swings, good for timing optimization | +| **Very High** | 50% | Extreme volatility, maximum optimization benefit | ### Step 6: Best Price Period Configure detection of favorable price windows. Three collapsible sections: **Period Settings:** + - Minimum period length (default: 30 min) - Maximum price level to include (default: CHEAP) - Gap tolerance: how many expensive intervals to bridge (default: 1) **Flexibility Settings:** + - Flex percentage (default: 15%): how far above the daily minimum a price can be to qualify - Minimum distance from daily average (default: 5%): ensures periods are meaningfully cheaper **Relaxation & Target:** + - Enable minimum period target (default: on) - Target periods per day (default: 2) - Relaxation attempts (default: 11): steps to loosen criteria if target not met @@ -114,12 +117,12 @@ Mirrors Best Price configuration but for expensive windows. Detects periods to * Configure when trend sensors report rising/falling: -| Setting | Default | Description | -|---------|---------|-------------| -| **Rising** | 3% | Future average this much above current → "rising" | -| **Strongly rising** | 9% | Future average far above current → "strongly_rising" | -| **Falling** | -3% | Future average this much below current → "falling" | -| **Strongly falling** | -9% | Future average far below current → "strongly_falling" | +| Setting | Default | Description | +| -------------------- | ------- | ----------------------------------------------------- | +| **Rising** | 3% | Future average this much above current → "rising" | +| **Strongly rising** | 9% | Future average far above current → "strongly_rising" | +| **Falling** | -3% | Future average this much below current → "falling" | +| **Strongly falling** | -9% | Future average far below current → "strongly_falling" | Thresholds are [volatility-adaptive](sensors.md#trend-sensors): automatically widened on volatile days to prevent constant state changes. @@ -138,12 +141,14 @@ The integration allows you to choose how average price sensors display their val #### Display Modes **Median (Default):** + - Shows the "middle value" when all prices are sorted - **Resistant to extreme spikes** - one expensive hour doesn't skew the result - Best for understanding **typical price levels** - Example: "What was the typical price today?" **Arithmetic Mean:** + - Shows the mathematical average of all prices - **Includes effect of spikes** - reflects actual cost if consuming evenly - Best for **cost calculations and budgeting** @@ -152,6 +157,7 @@ The integration allows you to choose how average price sensors display their val #### Why This Matters Consider a day with these hourly prices: + ``` 10, 12, 13, 15, 80 ct/kWh ``` @@ -172,6 +178,7 @@ The median tells you the price was **typically** around 13 ct/kWh (4 out of 5 ho ``` This means: + - ✅ You can change the display anytime without breaking automations - ✅ Automations can use both values for different purposes - ✅ No need to create template sensors for the "other" value @@ -179,6 +186,7 @@ This means: #### Affected Sensors This setting applies to: + - Daily average sensors (today, tomorrow) - 24-hour rolling averages (trailing, leading) - Hourly smoothed prices (current hour, next hour) @@ -189,12 +197,14 @@ See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examp #### Choosing Your Display **Choose Median if:** + - 👥 You show prices to users ("What's today like?") - 📊 You want dashboard values that represent typical conditions - 🎯 You compare price levels across days - 🔍 You analyze volatility (comparing typical vs extremes) **Choose Mean if:** + - 💰 You calculate costs and budgets - 📈 You forecast energy expenses - 🧮 You need mathematical accuracy for financial planning @@ -212,27 +222,27 @@ When enabled, these entities override the corresponding Options Flow settings: #### Best Price Period Settings -| Entity | Type | Range | Description | -|--------|------|-------|-------------| -| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals | -| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average | -| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold | -| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Entity | Type | Range | Description | +| ------------------------------------- | ------ | ---------- | ------------------------------------------------------ | +| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals | +| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average | +| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | +| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | +| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | +| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold | +| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | #### Peak Price Period Settings -| Entity | Type | Range | Description | -|--------|------|-------|-------------| -| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | -| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | -| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | -| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Entity | Type | Range | Description | +| ------------------------------------- | ------ | ---------- | ------------------------------------------------------ | +| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | +| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | +| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | +| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | +| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | +| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | +| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | ### How Runtime Overrides Work @@ -257,24 +267,25 @@ Each configuration entity includes a detailed description attribute explaining w ```yaml automation: - - alias: "Winter: Stricter Best Price Detection" - trigger: - - platform: time - at: "00:00:00" - condition: - - condition: template - value_template: "{{ now().month in [11, 12, 1, 2] }}" - action: - - service: number.set_value - target: - entity_id: number._best_price_flexibility - data: - value: 10 # Stricter than default 15% + - alias: "Winter: Stricter Best Price Detection" + trigger: + - platform: time + at: "00:00:00" + condition: + - condition: template + value_template: "{{ now().month in [11, 12, 1, 2] }}" + action: + - service: number.set_value + target: + entity_id: number._best_price_flexibility + data: + value: 10 # Stricter than default 15% ``` ### Recorder Optimization (Optional) These configuration entities are designed to minimize database impact: + - **EntityCategory.CONFIG** - Excluded from Long-Term Statistics - All attributes excluded from history recording - Only state value changes are recorded @@ -285,16 +296,17 @@ However, if you prefer to **completely exclude** these entities from the recorde ```yaml recorder: - exclude: - entity_globs: - # Exclude all Tibber Prices configuration entities - - number.*_best_price_* - - number.*_peak_price_* - - switch.*_best_price_* - - switch.*_peak_price_* + exclude: + entity_globs: + # Exclude all Tibber Prices configuration entities + - number.*_best_price_* + - number.*_peak_price_* + - switch.*_best_price_* + - switch.*_peak_price_* ``` This is especially useful if: + - You rarely change these settings - You want the smallest possible database footprint - You don't need to see the history graph for these entities diff --git a/docs/user/versioned_docs/version-v0.30.0/dashboard-examples.md b/docs/user/versioned_docs/version-v0.30.0/dashboard-examples.md index 777b059..9fd206b 100644 --- a/docs/user/versioned_docs/version-v0.30.0/dashboard-examples.md +++ b/docs/user/versioned_docs/version-v0.30.0/dashboard-examples.md @@ -12,13 +12,13 @@ Simple card showing current price with dynamic color: type: entities title: Current Electricity Price entities: - - entity: sensor._current_electricity_price - name: Current Price - icon: mdi:flash - - entity: sensor._current_price_rating - name: Price Rating - - entity: sensor._next_electricity_price - name: Next Price + - entity: sensor._current_electricity_price + name: Current Price + icon: mdi:flash + - entity: sensor._current_price_rating + name: Price Rating + - entity: sensor._next_electricity_price + name: Next Price ``` ## Period Status Cards @@ -28,14 +28,14 @@ Show when best/peak price periods are active: ```yaml type: horizontal-stack cards: - - type: entity - entity: binary_sensor._best_price_period - name: Best Price Active - icon: mdi:currency-eur-off - - type: entity - entity: binary_sensor._peak_price_period - name: Peak Price Active - icon: mdi:alert + - type: entity + entity: binary_sensor._best_price_period + name: Best Price Active + icon: mdi:currency-eur-off + - type: entity + entity: binary_sensor._peak_price_period + name: Peak Price Active + icon: mdi:alert ``` ## Custom Button Card Examples @@ -48,16 +48,16 @@ entity: sensor._current_price_level name: Price Level show_state: true styles: - card: - - background: | - [[[ - if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; - if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; - if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; - if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; - if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; - return 'var(--card-background-color)'; - ]]] + card: + - background: | + [[[ + if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)'; + if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)'; + if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)'; + if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)'; + if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)'; + return 'var(--card-background-color)'; + ]]] ``` ## Lovelace Layouts @@ -69,19 +69,19 @@ Optimized for mobile devices: ```yaml type: vertical-stack cards: - - type: custom:mini-graph-card - entities: - - entity: sensor._current_electricity_price - name: Today's Prices - hours_to_show: 24 - points_per_hour: 4 + - type: custom:mini-graph-card + entities: + - entity: sensor._current_electricity_price + name: Today's Prices + hours_to_show: 24 + points_per_hour: 4 - - type: glance - entities: - - entity: sensor._best_price_start - name: Best Period Starts - - entity: binary_sensor._best_price_period - name: Active Now + - type: glance + entities: + - entity: sensor._best_price_start + name: Best Period Starts + - entity: binary_sensor._best_price_period + name: Active Now ``` ### Desktop Dashboard @@ -93,25 +93,25 @@ type: grid columns: 3 square: false cards: - - type: custom:apexcharts-card - # See chart-examples.md for ApexCharts config + - type: custom:apexcharts-card + # See chart-examples.md for ApexCharts config - - type: vertical-stack - cards: - - type: entities - title: Current Status - entities: - - sensor._current_electricity_price - - sensor._current_price_rating + - type: vertical-stack + cards: + - type: entities + title: Current Status + entities: + - sensor._current_electricity_price + - sensor._current_price_rating - - type: vertical-stack - cards: - - type: entities - title: Statistics - entities: - - sensor._price_today - - sensor._today_s_lowest_price - - sensor._today_s_highest_price + - type: vertical-stack + cards: + - type: entities + title: Statistics + entities: + - sensor._price_today + - sensor._today_s_lowest_price + - sensor._today_s_highest_price ``` ## Icon Color Integration @@ -121,17 +121,17 @@ Using the `icon_color` attribute for dynamic colors: ```yaml type: custom:mushroom-chips-card chips: - - type: entity - entity: sensor._current_electricity_price - icon_color: "{{ state_attr('sensor._current_electricity_price', 'icon_color') }}" + - type: entity + entity: sensor._current_electricity_price + icon_color: "{{ state_attr('sensor._current_electricity_price', 'icon_color') }}" - - type: entity - entity: binary_sensor._best_price_period - icon_color: green + - type: entity + entity: binary_sensor._best_price_period + icon_color: green - - type: entity - entity: binary_sensor._peak_price_period - icon_color: red + - type: entity + entity: binary_sensor._peak_price_period + icon_color: red ``` See [Icon Colors](icon-colors.md) for detailed color mapping. @@ -144,21 +144,21 @@ Advanced interactive dashboard: type: picture-elements image: /local/electricity_dashboard_bg.png elements: - - type: state-label - entity: sensor._current_electricity_price - style: - top: 20% - left: 50% - font-size: 32px - font-weight: bold + - type: state-label + entity: sensor._current_electricity_price + style: + top: 20% + left: 50% + font-size: 32px + font-weight: bold - - type: state-badge - entity: binary_sensor._best_price_period - style: - top: 40% - left: 30% + - type: state-badge + entity: binary_sensor._best_price_period + style: + top: 40% + left: 30% - # Add more elements... + # Add more elements... ``` ## Auto-Entities Dynamic Lists @@ -168,21 +168,22 @@ Automatically list all price sensors: ```yaml type: custom:auto-entities card: - type: entities - title: All Price Sensors + type: entities + title: All Price Sensors filter: - include: - - entity_id: "sensor._*_price" - exclude: - - state: unavailable + include: + - entity_id: "sensor._*_price" + exclude: + - state: unavailable sort: - method: state - numeric: true + method: state + numeric: true ``` --- 💡 **Related:** + - [Chart Examples](chart-examples.md) - ApexCharts configurations - [Dynamic Icons](dynamic-icons.md) - Icon behavior - [Icon Colors](icon-colors.md) - Color attributes diff --git a/docs/user/versioned_docs/version-v0.30.0/dynamic-icons.md b/docs/user/versioned_docs/version-v0.30.0/dynamic-icons.md index ed53f24..3be732d 100644 --- a/docs/user/versioned_docs/version-v0.30.0/dynamic-icons.md +++ b/docs/user/versioned_docs/version-v0.30.0/dynamic-icons.md @@ -8,10 +8,10 @@ Many sensors in the Tibber Prices integration automatically change their icon ba Instead of having a fixed icon, some sensors update their icon to reflect their current state: -- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive -- **Price rating sensors** show thumbs up/down based on how the current price compares to average -- **Volatility sensors** show different chart types based on price stability -- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) +- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive +- **Price rating sensors** show thumbs up/down based on how the current price compares to average +- **Volatility sensors** show different chart types based on price stability +- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period) The icons change automatically - no configuration needed! @@ -26,10 +26,10 @@ To see which icon a sensor currently uses: **Common sensor types with dynamic icons:** -- Price level sensors (e.g., `current_price_level`) -- Price rating sensors (e.g., `current_price_rating`) -- Volatility sensors (e.g., `today_s_price_volatility`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Price level sensors (e.g., `current_price_level`) +- Price rating sensors (e.g., `current_price_rating`) +- Volatility sensors (e.g., `today_s_price_volatility`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) ## Using Dynamic Icons in Your Dashboard @@ -125,8 +125,8 @@ styles: This gives you both: -- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) -- ✅ Different color based on state (e.g., green when cheap, red when expensive) +- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive) +- ✅ Different color based on state (e.g., green when cheap, red when expensive) ## Icon Behavior Details @@ -134,24 +134,24 @@ This gives you both: Binary sensors may have different icons for different states: -- **ON state**: Typically shows an active/alert icon -- **OFF state**: May show different icons depending on whether future periods exist - - Has upcoming periods: Timer/waiting icon - - No upcoming periods: Sleep/inactive icon +- **ON state**: Typically shows an active/alert icon +- **OFF state**: May show different icons depending on whether future periods exist + - Has upcoming periods: Timer/waiting icon + - No upcoming periods: Sleep/inactive icon **Example:** `binary_sensor._best_price_period` -- When ON: Shows a piggy bank (good time to save money) -- When OFF with future periods: Shows a timer (waiting for next period) -- When OFF without future periods: Shows a sleep icon (no periods expected soon) +- When ON: Shows a piggy bank (good time to save money) +- When OFF with future periods: Shows a timer (waiting for next period) +- When OFF without future periods: Shows a sleep icon (no periods expected soon) ### State-Based Icons Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning: -- Lower/better values → More positive icons -- Higher/worse values → More cautionary icons -- Normal/average values → Neutral icons +- Lower/better values → More positive icons +- Higher/worse values → More cautionary icons +- Normal/average values → Neutral icons The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem. @@ -159,22 +159,22 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant **Icon not changing:** -- Wait for the sensor state to actually change (prices update every 15 minutes) -- Check in Developer Tools → States that the sensor state is changing -- If you've set a custom icon in your card, it will override the dynamic icon +- Wait for the sensor state to actually change (prices update every 15 minutes) +- Check in Developer Tools → States that the sensor state is changing +- If you've set a custom icon in your card, it will override the dynamic icon **Want to see the icon code:** -- Look at the entity in Developer Tools → States -- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) +- Look at the entity in Developer Tools → States +- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`) **Want different icons:** -- You can override icons in your card configuration (see examples above) -- Or create a template sensor with your own icon logic +- You can override icons in your card configuration (see examples above) +- Or create a template sensor with your own icon logic ## See Also -- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use dynamic icons in automations +- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use dynamic icons in automations diff --git a/docs/user/versioned_docs/version-v0.30.0/faq.md b/docs/user/versioned_docs/version-v0.30.0/faq.md index 260b09d..7cf4220 100644 --- a/docs/user/versioned_docs/version-v0.30.0/faq.md +++ b/docs/user/versioned_docs/version-v0.30.0/faq.md @@ -30,6 +30,7 @@ Yes! Use the **"Add another home"** option: ### Does this work without a Tibber subscription? No, you need: + - Active Tibber electricity contract - API token from [developer.tibber.com](https://developer.tibber.com/) @@ -40,10 +41,12 @@ The integration is free, but requires Tibber as your electricity provider. ### What are good values for price thresholds? **Default values work for most users:** + - High Price Threshold: 30% above average - Low Price Threshold: 15% below average **Adjust if:** + - You're in a market with high volatility → increase thresholds - You want more sensitive ratings → decrease thresholds - Seasonal changes → review every few months @@ -51,6 +54,7 @@ The integration is free, but requires Tibber as your electricity provider. ### How do I optimize Best Price Period detection? **Key parameters:** + - **Flex**: 15-20% is optimal (default 15%) - **Min Distance**: 5-10% recommended (default 5%) - **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default) @@ -61,12 +65,14 @@ See [Period Calculation](period-calculation.md) for detailed tuning guide. ### Why do I sometimes only get 1 period instead of 2? This happens on **high-price days** when: + - Few intervals meet your criteria - Relaxation is disabled - Flex is too low - Min Distance is too strict **Solutions:** + 1. Enable relaxation (recommended) 2. Increase flex to 20-25% 3. Reduce min_distance to 3-5% @@ -77,6 +83,7 @@ This happens on **high-price days** when: ### Sensors show "unavailable" **Common causes:** + 1. **API Token invalid** → Check token at developer.tibber.com 2. **No internet connection** → Check HA network 3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com) @@ -85,6 +92,7 @@ This happens on **high-price days** when: ### Best Price Period is ON all day This means **all intervals meet your criteria** (very cheap day!): + - Not an error - enjoy the low prices! - Consider tightening filters (lower flex, higher min_distance) - Or add automation to only run during first detected period @@ -94,10 +102,11 @@ This means **all intervals meet your criteria** (very cheap day!): **Currency** is determined by your Tibber subscription (cannot be changed). **Display mode** (base vs. subunit) is configurable: + - Configure in: `Settings > Devices & Services > Tibber Prices > Configure` - Options: - - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) - - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) + - **Base currency**: €/kWh, kr/kWh (decimal values like 0.25) + - **Subunit**: ct/kWh, øre/kWh (larger values like 25.00) - Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency If you see unexpected units, check your configuration in the integration options. @@ -105,6 +114,7 @@ If you see unexpected units, check your configuration in the integration options ### Tomorrow data not appearing at all **Check:** + 1. Your Tibber home has hourly price contract (not fixed price) 2. API token has correct permissions 3. Integration logs for API errors (`/config/home-assistant.log`) @@ -118,18 +128,18 @@ If you see unexpected units, check your configuration in the integration options ```yaml automation: - - alias: "Dishwasher during Best Price" - trigger: - - platform: state - entity_id: binary_sensor._best_price_period - to: "on" - condition: - - condition: time - after: "20:00:00" # Only start after 8 PM - action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - alias: "Dishwasher during Best Price" + trigger: + - platform: state + entity_id: binary_sensor._best_price_period + to: "on" + condition: + - condition: time + after: "20:00:00" # Only start after 8 PM + action: + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` See [Automation Examples](automation-examples.md) for more recipes. @@ -140,19 +150,20 @@ Yes! Use Peak Price Period binary sensor: ```yaml automation: - - alias: "Disable charging during peak prices" - trigger: - - platform: state - entity_id: binary_sensor._peak_price_period - to: "on" - action: - - service: switch.turn_off - target: - entity_id: switch.ev_charger + - alias: "Disable charging during peak prices" + trigger: + - platform: state + entity_id: binary_sensor._peak_price_period + to: "on" + action: + - service: switch.turn_off + target: + entity_id: switch.ev_charger ``` --- 💡 **Still need help?** + - [Troubleshooting Guide](troubleshooting.md) - [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues) diff --git a/docs/user/versioned_docs/version-v0.30.0/glossary.md b/docs/user/versioned_docs/version-v0.30.0/glossary.md index ab5d9b5..e8c42d5 100644 --- a/docs/user/versioned_docs/version-v0.30.0/glossary.md +++ b/docs/user/versioned_docs/version-v0.30.0/glossary.md @@ -26,8 +26,9 @@ Quick reference for terms used throughout the documentation. **Config Entry ID** (also: `entry_id`) : A unique identifier assigned by Home Assistant to each configured integration instance. When this integration is used with multiple Tibber homes, each home gets its own Config Entry ID. All actions (`get_chartdata`, `get_apexcharts_yaml`, etc.) require this value as the `entry_id` parameter so that Home Assistant knows which home to query. - - **In the Action UI**: The field appears as a dropdown — select your home and HA fills in the ID automatically. - - **In YAML**: Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** menu, and choose **"Copy Config Entry ID"**. + +- **In the Action UI**: The field appears as a dropdown — select your home and HA fills in the ID automatically. +- **In YAML**: Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** menu, and choose **"Copy Config Entry ID"**. **Currency Display Mode** : Configurable setting for how prices are shown. Choose base currency (€, kr) or subunit (ct, øre). Smart defaults apply: EUR → subunit, NOK/SEK/DKK → base. @@ -114,6 +115,7 @@ Quick reference for terms used throughout the documentation. --- 💡 **See Also:** + - [Core Concepts](concepts.md) - In-depth explanations - [Sensors](sensors.md) - How sensors use these concepts - [Period Calculation](period-calculation.md) - Deep dive into period detection diff --git a/docs/user/versioned_docs/version-v0.30.0/icon-colors.md b/docs/user/versioned_docs/version-v0.30.0/icon-colors.md index 0ee1145..f3cd7b7 100644 --- a/docs/user/versioned_docs/version-v0.30.0/icon-colors.md +++ b/docs/user/versioned_docs/version-v0.30.0/icon-colors.md @@ -16,17 +16,17 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example: -- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive -- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price -- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high +- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive +- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price +- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high ### Why CSS Variables? Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages: -- ✅ **Automatic theme adaptation** - Colors change with light/dark mode -- ✅ **Consistent with your theme** - Uses your theme's color scheme -- ✅ **Future-proof** - Works with custom themes and future HA updates +- ✅ **Automatic theme adaptation** - Colors change with light/dark mode +- ✅ **Consistent with your theme** - Uses your theme's color scheme +- ✅ **Future-proof** - Works with custom themes and future HA updates You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below). @@ -40,12 +40,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a **Common sensor types with icon_color:** -- Price level sensors (e.g., `current_price_level`) -- Price rating sensors (e.g., `current_price_rating`) -- Volatility sensors (e.g., `today_s_price_volatility`) -- Price outlook sensors (e.g., `price_outlook_3h`) -- Binary sensors (e.g., `best_price_period`, `peak_price_period`) -- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) +- Price level sensors (e.g., `current_price_level`) +- Price rating sensors (e.g., `current_price_rating`) +- Volatility sensors (e.g., `today_s_price_volatility`) +- Price outlook sensors (e.g., `price_outlook_3h`) +- Binary sensors (e.g., `best_price_period`, `peak_price_period`) +- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`) The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray. @@ -53,15 +53,15 @@ The colors adapt to the sensor's state - cheaper prices typically show green, ex **Use `icon_color` when:** -- ✅ You can apply the CSS variable directly (icons, text colors, borders) -- ✅ Your card supports CSS variable substitution -- ✅ You want simple, clean code without if/else logic +- ✅ You can apply the CSS variable directly (icons, text colors, borders) +- ✅ Your card supports CSS variable substitution +- ✅ You want simple, clean code without if/else logic **Use the state value directly when:** -- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) -- ⚠️ You need different colors than what `icon_color` provides -- ⚠️ You're building complex conditional logic anyway +- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency) +- ⚠️ You need different colors than what `icon_color` provides +- ⚠️ You're building complex conditional logic anyway **Example of when NOT to use icon_color:** @@ -297,12 +297,12 @@ cards: The integration uses Home Assistant's standard CSS variables for theme compatibility: -- `var(--success-color)` - Green (good/cheap/low) -- `var(--info-color)` - Blue (informational) -- `var(--warning-color)` - Orange (caution/expensive) -- `var(--error-color)` - Red (alert/very expensive/high) -- `var(--state-icon-color)` - Gray (neutral/normal) -- `var(--disabled-color)` - Light gray (no data/inactive) +- `var(--success-color)` - Green (good/cheap/low) +- `var(--info-color)` - Blue (informational) +- `var(--warning-color)` - Orange (caution/expensive) +- `var(--error-color)` - Red (alert/very expensive/high) +- `var(--state-icon-color)` - Gray (neutral/normal) +- `var(--disabled-color)` - Light gray (no data/inactive) These automatically adapt to your theme's light/dark mode and custom color schemes. @@ -428,22 +428,22 @@ styles: **Icons not changing color:** -- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) -- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) -- Verify your Home Assistant theme supports the CSS variables +- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod) +- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States) +- Verify your Home Assistant theme supports the CSS variables **Colors look wrong:** -- The colors are theme-dependent. Try switching themes to see if they appear correctly -- Some custom themes may override the standard CSS variables with unexpected colors +- The colors are theme-dependent. Try switching themes to see if they appear correctly +- Some custom themes may override the standard CSS variables with unexpected colors **Want different colors?** -- You can override the colors in your theme configuration -- Or use conditional logic in your card templates based on the state value instead of `icon_color` +- You can override the colors in your theme configuration +- Or use conditional logic in your card templates based on the state value instead of `icon_color` ## See Also -- [Sensors Reference](sensors.md) - Complete list of available sensors -- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations -- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings +- [Sensors Reference](sensors.md) - Complete list of available sensors +- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations +- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings diff --git a/docs/user/versioned_docs/version-v0.30.0/installation.md b/docs/user/versioned_docs/version-v0.30.0/installation.md index 00cc47b..e745334 100644 --- a/docs/user/versioned_docs/version-v0.30.0/installation.md +++ b/docs/user/versioned_docs/version-v0.30.0/installation.md @@ -16,10 +16,10 @@ 2. Go to **Integrations** 3. Click the **⋮** menu (top right) → **Custom repositories** 4. Add the repository URL: - ``` - https://github.com/jpawlowski/hass.tibber_prices - ``` - Category: **Integration** + ``` + https://github.com/jpawlowski/hass.tibber_prices + ``` + Category: **Integration** 5. Click **Add** 6. Find **Tibber Price Information & Ratings** in the integration list 7. Click **Download** @@ -42,16 +42,16 @@ If you prefer not to use HACS: 1. Download the [latest release](https://github.com/jpawlowski/hass.tibber_prices/releases/latest) from GitHub 2. Extract the `custom_components/tibber_prices/` folder 3. Copy it to your Home Assistant `config/custom_components/` directory: - ``` - config/ - └── custom_components/ - └── tibber_prices/ - ├── __init__.py - ├── manifest.json - ├── sensor/ - ├── binary_sensor/ - └── ... - ``` + ``` + config/ + └── custom_components/ + └── tibber_prices/ + ├── __init__.py + ├── manifest.json + ├── sensor/ + ├── binary_sensor/ + └── ... + ``` 4. **Restart Home Assistant** 5. Continue with [Configuration](configuration.md) diff --git a/docs/user/versioned_docs/version-v0.30.0/intro.md b/docs/user/versioned_docs/version-v0.30.0/intro.md index 243d9f6..29c94ad 100644 --- a/docs/user/versioned_docs/version-v0.30.0/intro.md +++ b/docs/user/versioned_docs/version-v0.30.0/intro.md @@ -12,16 +12,16 @@ This is an independent, community-maintained custom integration. It is **not** a ## 📚 Documentation -- **[Installation](installation.md)** - How to install via HACS and configure the integration -- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds -- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured -- **[Sensors](sensors.md)** - Available sensors, their states, and attributes -- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes -- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards -- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them -- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots -- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes -- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions +- **[Installation](installation.md)** - How to install via HACS and configure the integration +- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds +- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured +- **[Sensors](sensors.md)** - Available sensors, their states, and attributes +- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes +- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards +- **[Actions](actions.md)** - Custom actions (service endpoints) and how to use them +- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots +- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes +- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions ## 🚀 Quick Start @@ -33,26 +33,26 @@ This is an independent, community-maintained custom integration. It is **not** a ## ✨ Key Features -- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking -- **Statistical analysis** - Trailing/leading 24h averages for context -- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds -- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) -- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) -- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization -- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency +- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking +- **Statistical analysis** - Trailing/leading 24h averages for context +- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds +- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md)) +- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md)) +- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization +- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency ## 🔗 Useful Links -- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) -- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) -- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) -- [Home Assistant Community](https://community.home-assistant.io/) +- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices) +- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues) +- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases) +- [Home Assistant Community](https://community.home-assistant.io/) ## 🤝 Need Help? -- Check the [Troubleshooting Guide](troubleshooting.md) -- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) -- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed +- Check the [Troubleshooting Guide](troubleshooting.md) +- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues) +- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed --- diff --git a/docs/user/versioned_docs/version-v0.30.0/period-calculation.md b/docs/user/versioned_docs/version-v0.30.0/period-calculation.md index 696ea36..b5381cd 100644 --- a/docs/user/versioned_docs/version-v0.30.0/period-calculation.md +++ b/docs/user/versioned_docs/version-v0.30.0/period-calculation.md @@ -6,19 +6,19 @@ Learn how Best Price and Peak Price periods work, and how to configure them for ## Table of Contents -- [Quick Start](#quick-start) -- [How It Works](#how-it-works) -- [Configuration Guide](#configuration-guide) -- [Understanding Relaxation](#understanding-relaxation) -- [Common Scenarios](#common-scenarios) -- [Troubleshooting](#troubleshooting) - - [Fewer Periods Than Configured](#fewer-periods-than-configured) - - [No Periods Found](#no-periods-found) - - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) - - [Understanding Sensor Attributes](#understanding-sensor-attributes) - - [Midnight Price Classification Changes](#midnight-price-classification-changes) -- [Advanced Topics](#advanced-topics) -- [Advanced Topics](#advanced-topics) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Configuration Guide](#configuration-guide) +- [Understanding Relaxation](#understanding-relaxation) +- [Common Scenarios](#common-scenarios) +- [Troubleshooting](#troubleshooting) + - [Fewer Periods Than Configured](#fewer-periods-than-configured) + - [No Periods Found](#no-periods-found) + - [Periods Split Into Small Pieces](#periods-split-into-small-pieces) + - [Understanding Sensor Attributes](#understanding-sensor-attributes) + - [Midnight Price Classification Changes](#midnight-price-classification-changes) +- [Advanced Topics](#advanced-topics) +- [Advanced Topics](#advanced-topics) --- @@ -28,8 +28,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water +- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior @@ -47,16 +47,19 @@ Out of the box, the integration: The integration sets different **initial defaults** because the features serve different purposes: **Best Price (60 min, 15% flex):** + - Longer duration ensures appliances can complete their cycles - Stricter flex (15%) focuses on genuinely cheap times - Use case: Running dishwasher, EV charging, water heating **Peak Price (30 min, 20% flex):** + - Shorter duration acceptable for early warnings - More flexible (20%) catches price spikes earlier - Use case: Alerting to expensive periods, even brief ones **You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios. + ### Example Timeline @@ -157,7 +160,7 @@ Default: 60 minutes minimum You can optionally require: -- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" +- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)" #### 5. Automatic Price Spike Smoothing @@ -171,9 +174,10 @@ Result: Continuous period 00:00-01:15 instead of split periods ``` **Important:** -- Original prices are always preserved (min/max/avg show real values) -- Smoothing only affects which intervals are combined into periods -- The attribute `period_interval_smoothed_count` shows if smoothing was active + +- Original prices are always preserved (min/max/avg show real values) +- Smoothing only affects which intervals are combined into periods +- The attribute `period_interval_smoothed_count` shows if smoothing was active ### Visual Example @@ -213,8 +217,8 @@ peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX **When to adjust:** -- **Increase (20-25%)** → Find more/longer periods -- **Decrease (5-10%)** → Find only the very best/worst times +- **Increase (20-25%)** → Find more/longer periods +- **Decrease (5-10%)** → Find only the very best/worst times **💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation – it adapts automatically to each day's price pattern. @@ -231,8 +235,8 @@ peak_price_min_period_length: 30 **When to adjust:** -- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) -- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) +- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles) +- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks) #### Distance from Average @@ -247,8 +251,8 @@ peak_price_min_distance_from_avg: 5 **When to adjust:** -- **Increase (5-10%)** → Only show clearly better times -- **Decrease (0-1%)** → Show any time below/above average +- **Increase (5-10%)** → Only show clearly better times +- **Decrease (0-1%)** → Show any time below/above average **ℹ️ Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled. @@ -291,9 +295,9 @@ When you're not happy with the default behavior, adjust settings in this order: If you're not finding enough periods: ```yaml -enable_min_periods_best: true # Already default! -min_periods_best: 2 # Already default! -relaxation_attempts_best: 11 # Already default! +enable_min_periods_best: true # Already default! +min_periods_best: 2 # Already default! +relaxation_attempts_best: 11 # Already default! ``` **Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning. @@ -327,7 +331,7 @@ best_price_flex: 10 # Decrease from 15% for stricter selection Only if periods seem "mediocre" (not really cheap/expensive): ```yaml -best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality +best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality ``` **⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days. @@ -337,7 +341,7 @@ best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality Only if you want absolute quality requirements: ```yaml -best_price_max_level: cheap # Only show objectively CHEAP periods +best_price_max_level: cheap # Only show objectively CHEAP periods ``` **⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!** @@ -376,11 +380,13 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods ### Why Relaxation Is Better Than Manual Tweaking **Problem with manual settings:** + - You set flex to 25% → Works great on Monday (volatile prices) - Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap - You're stuck with one setting for all days **Solution with relaxation:** + - Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓ - Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓ - Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓ @@ -392,6 +398,7 @@ Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up. **Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means: + - Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts) - Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts) @@ -439,10 +446,10 @@ Each attempt adds +3% flexibility and tries two filter combinations. The system ### Choosing the Number of Attempts -- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) -- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) -- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) -- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) +- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak) +- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex) +- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations) +- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any) #### Per-Day Independence @@ -475,9 +482,9 @@ best_price_min_distance_from_avg: 5 # (default) **What you get:** -- 1-3 periods per day with prices ≤ MIN + 15% -- Each period at least 1 hour long -- All periods at least 5% cheaper than daily average +- 1-3 periods per day with prices ≤ MIN + 15% +- Each period at least 1 hour long +- All periods at least 5% cheaper than daily average **Automation example:** @@ -508,7 +515,7 @@ This is **expected behavior** on days with very uniform electricity prices. When ```yaml min_periods_configured: 2 periods_found_total: 1 -flat_days_detected: 1 # Uniform prices today → 1 period is the right answer +flat_days_detected: 1 # Uniform prices today → 1 period is the right answer ``` You don't need to change anything. This is the integration protecting you from artificial periods. @@ -518,14 +525,16 @@ You don't need to change anything. This is the integration protecting you from a Relaxation tried all configured attempts but couldn't reach your target. Options: 1. **Increase relaxation attempts** (tries more flexibility levels before giving up) - ```yaml - relaxation_attempts_best: 12 # Default: 11 - ``` + + ```yaml + relaxation_attempts_best: 12 # Default: 11 + ``` 2. **Reduce minimum period count** - ```yaml - min_periods_best: 1 # Only require 1 period per day - ``` + + ```yaml + min_periods_best: 1 # Only require 1 period per day + ``` 3. **Check filter settings** – very strict `best_price_min_distance_from_avg` values block relaxation @@ -538,24 +547,26 @@ Relaxation tried all configured attempts but couldn't reach your target. Options **Common Solutions:** 1. **Check if relaxation is enabled** - ```yaml - enable_min_periods_best: true # Should be true (default) - min_periods_best: 2 # Try to find at least 2 periods - ``` + + ```yaml + enable_min_periods_best: true # Should be true (default) + min_periods_best: 2 # Try to find at least 2 periods + ``` 2. **If still no periods, check filters** - - Look at sensor attributes: `relaxation_active` and `relaxation_level` - - If relaxation exhausted all attempts: Filters too strict or flat price day + - Look at sensor attributes: `relaxation_active` and `relaxation_level` + - If relaxation exhausted all attempts: Filters too strict or flat price day 3. **Try increasing flexibility slightly** - ```yaml - best_price_flex: 20 # Increase from default 15% - ``` + + ```yaml + best_price_flex: 20 # Increase from default 15% + ``` 4. **Or reduce period length requirement** - ```yaml - best_price_min_period_length: 45 # Reduce from default 60 minutes - ``` + ```yaml + best_price_min_period_length: 45 # Reduce from default 60 minutes + ``` ### Periods Split Into Small Pieces @@ -564,20 +575,22 @@ Relaxation tried all configured attempts but couldn't reach your target. Options **Common Solutions:** 1. **If using level filter, add gap tolerance** - ```yaml - best_price_max_level: cheap - best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals - ``` + + ```yaml + best_price_max_level: cheap + best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals + ``` 2. **Slightly increase flexibility** - ```yaml - best_price_flex: 20 # From 15% → captures wider price range - ``` + + ```yaml + best_price_flex: 20 # From 15% → captures wider price range + ``` 3. **Check for price spikes** - - Automatic smoothing should handle this - - Check attribute: `period_interval_smoothed_count` - - If 0: Not isolated spikes, but real price levels + - Automatic smoothing should handle this + - Check attribute: `period_interval_smoothed_count` + - If 0: Not isolated spikes, but real price levels ### Understanding Sensor Attributes @@ -587,26 +600,26 @@ Relaxation tried all configured attempts but couldn't reach your target. Options # Entity: binary_sensor._best_price_period # When "on" (period active): -start: "2025-11-11T02:00:00+01:00" # Period start time -end: "2025-11-11T05:00:00+01:00" # Period end time -duration_minutes: 180 # Duration in minutes -price_mean: 18.5 # Arithmetic mean price in the period -price_median: 18.3 # Median price in the period -rating_level: "LOW" # All intervals have LOW rating +start: "2025-11-11T02:00:00+01:00" # Period start time +end: "2025-11-11T05:00:00+01:00" # Period end time +duration_minutes: 180 # Duration in minutes +price_mean: 18.5 # Arithmetic mean price in the period +price_median: 18.3 # Median price in the period +rating_level: "LOW" # All intervals have LOW rating # Relaxation info (shows if filter loosening was needed): -relaxation_active: true # This day needed relaxation -relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed +relaxation_active: true # This day needed relaxation +relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed # Calculation summary (always shown – diagnostic overview of this calculation run): -min_periods_configured: 2 # What you configured as target -periods_found_total: 3 # What was actually found across all days +min_periods_configured: 2 # What you configured as target +periods_found_total: 3 # What was actually found across all days # Optional (only shown when relevant): -period_interval_smoothed_count: 2 # Number of price spikes smoothed -period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated -flat_days_detected: 1 # Days where prices were so flat that 1 period is enough -relaxation_incomplete: true # Some days couldn't reach the configured target +period_interval_smoothed_count: 2 # Number of price spikes smoothed +period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated +flat_days_detected: 1 # Days where prices were so flat that 1 period is enough +relaxation_incomplete: true # Some days couldn't reach the configured target ``` #### What the diagnostic attributes mean @@ -616,13 +629,13 @@ relaxation_incomplete: true # Some days couldn't reach the configured ta These two values together quickly show whether the calculation achieved its goal: ```yaml -min_periods_configured: 2 # You asked for 2 periods per day -periods_found_total: 6 # 3 days × 2 periods = fully satisfied ✅ +min_periods_configured: 2 # You asked for 2 periods per day +periods_found_total: 6 # 3 days × 2 periods = fully satisfied ✅ ``` ```yaml min_periods_configured: 2 -periods_found_total: 5 # 3 days, but one day got only 1 period +periods_found_total: 5 # 3 days, but one day got only 1 period ``` Note that `periods_found_total` counts **all periods across today and tomorrow** – so 4 on a two-day view means 2 per day on average. @@ -634,7 +647,7 @@ This is the most important diagnostic for days with very uniform prices (e.g. su ```yaml min_periods_configured: 2 periods_found_total: 1 -flat_days_detected: 1 # ← This explains why you got 1 instead of 2 +flat_days_detected: 1 # ← This explains why you got 1 instead of 2 ``` When prices barely change across the day – typically a variation of less than 10% – the integration automatically reduces the target from your configured value to 1. There is no meaningful second "best price window" when all prices are essentially equal. @@ -648,10 +661,11 @@ This flag appears when even after all relaxation attempts, at least one day coul ```yaml min_periods_configured: 2 periods_found_total: 1 -relaxation_incomplete: true # ← Relaxation tried everything, still short +relaxation_incomplete: true # ← Relaxation tried everything, still short ``` This is most common on very flat days (see above) or with very strict filter settings. If you see this repeatedly on normal days, consider: + - Reducing `min_periods_best` to 1 - Increasing `relaxation_attempts_best` - Checking if your `best_price_min_distance_from_avg` is too high @@ -665,12 +679,14 @@ This is most common on very flat days (see above) or with very strict filter set This is **mathematically correct behavior** caused by how electricity prices are set in the day-ahead market: **Market Timing:** + - The EPEX SPOT Day-Ahead auction closes at **12:00 CET** each day - **All prices** for the next day (00:00-23:45) are set at this moment - Late-day intervals (23:45) are priced **~36 hours before delivery** - Early-day intervals (00:00) are priced **~12 hours before delivery** **Why Prices Jump at Midnight:** + 1. **Forecast Uncertainty:** Weather, demand, and renewable generation forecasts are more uncertain 36 hours ahead than 12 hours ahead 2. **Risk Buffer:** Late-day prices include a risk premium for this uncertainty 3. **Independent Days:** Each day has its own min/max/avg calculated from its 96 intervals @@ -696,6 +712,7 @@ Daily average: 19 ct/kWh ``` **When This Occurs:** + - **Low-volatility days:** When price span is narrow (< 5 ct/kWh) - **Stable weather:** Similar conditions across multiple days - **Market transitions:** Switching between high/low demand seasons @@ -706,8 +723,9 @@ Check the volatility sensors to understand if a period flip is meaningful: ```yaml # Check daily volatility (available in integration) -sensor._today_s_price_volatility: 8.2% # Low volatility -sensor._tomorrow_s_price_volatility: 7.9% # Also low +sensor._today_s_price_volatility: 8.2% # Low volatility +sensor._tomorrow_s_price_volatility: 7.9% # Also low + # Low volatility (< 15%) means: # - Small absolute price differences between periods @@ -773,15 +791,16 @@ Each period sensor exposes day volatility and price statistics: ```yaml binary_sensor._best_price_period: - day_volatility_%: 8.2 # Volatility % of the period's day - day_price_min: 1800.0 # Minimum price of the day (ct/kWh) - day_price_max: 2200.0 # Maximum price of the day (ct/kWh) - day_price_span: 400.0 # Difference (max - min) in ct + day_volatility_%: 8.2 # Volatility % of the period's day + day_price_min: 1800.0 # Minimum price of the day (ct/kWh) + day_price_max: 2200.0 # Maximum price of the day (ct/kWh) + day_price_span: 400.0 # Difference (max - min) in ct ``` These attributes allow automations to check: "Is the classification meaningful on this particular day?" **Summary:** + - ✅ **Expected behavior:** Periods are evaluated per-day, midnight is a natural boundary - ✅ **Market reality:** Late-day prices have more uncertainty than early-day prices - ✅ **Solution:** Use volatility sensors, absolute price thresholds, or per-period day volatility attributes @@ -792,8 +811,8 @@ These attributes allow automations to check: "Is the classification meaningful o For advanced configuration patterns and technical deep-dive, see: -- [Automation Examples](./automation-examples.md) - Real-world automation patterns -- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations +- [Automation Examples](./automation-examples.md) - Real-world automation patterns +- [Actions](./actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations ### Quick Reference @@ -818,11 +837,11 @@ The Tibber API provides price levels for each 15-minute interval: **Levels (based on trailing 24h average):** -- `VERY_CHEAP` - Significantly below average -- `CHEAP` - Below average -- `NORMAL` - Around average -- `EXPENSIVE` - Above average -- `VERY_EXPENSIVE` - Significantly above average +- `VERY_CHEAP` - Significantly below average +- `CHEAP` - Below average +- `NORMAL` - Around average +- `EXPENSIVE` - Above average +- `VERY_EXPENSIVE` - Significantly above average --- diff --git a/docs/user/versioned_docs/version-v0.30.0/sensors.md b/docs/user/versioned_docs/version-v0.30.0/sensors.md index 83b9222..2c752ed 100644 --- a/docs/user/versioned_docs/version-v0.30.0/sensors.md +++ b/docs/user/versioned_docs/version-v0.30.0/sensors.md @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average +- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -29,15 +29,15 @@ The integration provides several sensors that calculate average electricity pric #### Available Average Sensors -| Sensor | Description | Time Window | -|--------|-------------|-------------| -| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | -| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | -| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | -| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | -| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | -| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | -| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | +| Sensor | Description | Time Window | +| -------------------------- | -------------------------------------- | ------------------------------- | +| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | +| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | +| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | +| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | +| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | +| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | +| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | #### Configurable Display: Median vs Mean @@ -66,8 +66,8 @@ You can choose which value is displayed in the sensor state: 2. Click **Configure** on your home 3. Navigate to **Step 6: Average Sensor Display Settings** 4. Choose between: - - **Median** (default) - Shows typical price level, resistant to spikes - - **Arithmetic Mean** - Shows actual mathematical average + - **Median** (default) - Shows typical price level, resistant to spikes + - **Arithmetic Mean** - Shows actual mathematical average **Important:** Both values are **always available** as sensor attributes, regardless of your choice! This ensures your automations continue to work if you change the display setting. @@ -78,22 +78,22 @@ Both `price_mean` and `price_median` are always available as attributes: ```yaml # Example: Get both values regardless of display setting sensor: - - platform: template - sensors: - daily_price_analysis: - friendly_name: "Daily Price Analysis" - value_template: > - {% set median = state_attr('sensor._price_today', 'price_median') %} - {% set mean = state_attr('sensor._price_today', 'price_mean') %} - {% set current = states('sensor._current_electricity_price') | float %} + - platform: template + sensors: + daily_price_analysis: + friendly_name: "Daily Price Analysis" + value_template: > + {% set median = state_attr('sensor._price_today', 'price_median') %} + {% set mean = state_attr('sensor._price_today', 'price_mean') %} + {% set current = states('sensor._current_electricity_price') | float %} - {% if current < median %} - Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) - {% elif current < mean %} - Typical price range - {% else %} - Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) - {% endif %} + {% if current < median %} + Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper) + {% elif current < mean %} + Typical price range + {% else %} + Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive) + {% endif %} ``` #### Practical Examples @@ -104,21 +104,21 @@ Run dishwasher only when price is significantly below the daily typical level: ```yaml automation: - - alias: "Start Dishwasher When Cheap" - trigger: - - platform: state - entity_id: binary_sensor._best_price_period - to: "on" - condition: - # Only if current price is at least 20% below typical (median) - - condition: template - value_template: > - {% set current = states('sensor._current_electricity_price') | float %} - {% set median = state_attr('sensor._price_today', 'price_median') | float %} - {{ current < (median * 0.8) }} - action: - - service: switch.turn_on - entity_id: switch.dishwasher + - alias: "Start Dishwasher When Cheap" + trigger: + - platform: state + entity_id: binary_sensor._best_price_period + to: "on" + condition: + # Only if current price is at least 20% below typical (median) + - condition: template + value_template: > + {% set current = states('sensor._current_electricity_price') | float %} + {% set median = state_attr('sensor._price_today', 'price_median') | float %} + {{ current < (median * 0.8) }} + action: + - service: switch.turn_on + entity_id: switch.dishwasher ``` **Example 2: Cost-aware heating control** @@ -127,20 +127,20 @@ Use mean for actual cost calculations: ```yaml automation: - - alias: "Heating Budget Control" - trigger: - - platform: time - at: "06:00:00" - action: - # Calculate expected daily heating cost - - variables: - mean_price: "{{ state_attr('sensor._price_today', 'price_mean') | float }}" - heating_kwh_per_day: 15 # Estimated consumption - daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" - - service: notify.mobile_app - data: - title: "Heating Cost Estimate" - message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" + - alias: "Heating Budget Control" + trigger: + - platform: time + at: "06:00:00" + action: + # Calculate expected daily heating cost + - variables: + mean_price: "{{ state_attr('sensor._price_today', 'price_mean') | float }}" + heating_kwh_per_day: 15 # Estimated consumption + daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}" + - service: notify.mobile_app + data: + title: "Heating Cost Estimate" + message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)" ``` **Example 3: Smart charging based on rolling average** @@ -149,60 +149,61 @@ Use trailing average to understand recent price trends: ```yaml automation: - - alias: "EV Charging - Price Trend Based" - trigger: - - platform: state - entity_id: sensor.ev_battery_level - condition: - # Start charging if current price < 90% of recent 24h average - - condition: template - value_template: > - {% set current = states('sensor._current_electricity_price') | float %} - {% set trailing_avg = state_attr('sensor._price_trailing_24h', 'price_median') | float %} - {{ current < (trailing_avg * 0.9) }} - # And battery < 80% - - condition: numeric_state - entity_id: sensor.ev_battery_level - below: 80 - action: - - service: switch.turn_on - entity_id: switch.ev_charger + - alias: "EV Charging - Price Trend Based" + trigger: + - platform: state + entity_id: sensor.ev_battery_level + condition: + # Start charging if current price < 90% of recent 24h average + - condition: template + value_template: > + {% set current = states('sensor._current_electricity_price') | float %} + {% set trailing_avg = state_attr('sensor._price_trailing_24h', 'price_median') | float %} + {{ current < (trailing_avg * 0.9) }} + # And battery < 80% + - condition: numeric_state + entity_id: sensor.ev_battery_level + below: 80 + action: + - service: switch.turn_on + entity_id: switch.ev_charger ``` #### Key Attributes All average sensors provide these attributes: -| Attribute | Description | Example | -|-----------|-------------|---------| -| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | -| `price_median` | Median value (always available) | 22.1 ct/kWh | -| `interval_count` | Number of intervals included | 96 | -| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | +| Attribute | Description | Example | +| ---------------- | ---------------------------------- | ------------------------- | +| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh | +| `price_median` | Median value (always available) | 22.1 ct/kWh | +| `interval_count` | Number of intervals included | 96 | +| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 | **Note:** The `price_mean` and `price_median` attributes are **always present** regardless of which value you configured for display. This ensures automation compatibility when changing the display setting. #### When to Use Which Value **Use Median for:** + - ✅ Comparing "typical" price levels across days - ✅ Determining if current price is unusually high/low - ✅ User-facing displays ("What was today like?") - ✅ Volatility analysis (comparing typical vs extremes) **Use Mean for:** + - ✅ Cost calculations and budgeting - ✅ Energy cost estimations - ✅ Comparing actual average costs between periods - ✅ Financial planning and forecasting **Both values tell different stories:** + - High median + much higher mean = Expensive spikes occurred - Low median + higher mean = Generally cheap with occasional spikes - Similar median and mean = Stable prices (low volatility) - - ## Volatility Sensors Volatility sensors help you understand how much electricity prices fluctuate over a given period. Instead of just looking at the absolute price, they measure the **relative price variation**, which is a great indicator of whether it's a good day for price-based energy optimization. @@ -217,21 +218,23 @@ The sensor's state can be `low`, `moderate`, `high`, or `very_high`, based on co ### Available Volatility Sensors -| Sensor | Description | Time Window | -|---|---|---| -| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | -| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | -| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | -| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | +| Sensor | Description | Time Window | +| ------------------------------------- | ----------------------------------------- | ---------------------- | +| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | +| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | +| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | +| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | ### Configuration You can adjust the CV thresholds that determine the volatility level: + 1. Go to **Settings → Devices & Services → Tibber Prices**. 2. Click **Configure**. 3. Go to the **Price Volatility Thresholds** step. Default thresholds are: + - **Moderate:** 15% - **High:** 30% - **Very High:** 50% @@ -240,14 +243,14 @@ Default thresholds are: All volatility sensors provide these attributes: -| Attribute | Description | Example | -|---|---|---| -| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` | -| `price_spread` | The difference between the highest and lowest price | `12.3` | -| `price_min` | The lowest price in the period | `10.2` | -| `price_max` | The highest price in the period | `22.5` | -| `price_mean` | The arithmetic mean of all prices in the period | `15.1` | -| `interval_count` | Number of price intervals included in the calculation | `96` | +| Attribute | Description | Example | +| ------------------------------- | ----------------------------------------------------- | ------- | +| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` | +| `price_spread` | The difference between the highest and lowest price | `12.3` | +| `price_min` | The lowest price in the period | `10.2` | +| `price_max` | The highest price in the period | `22.5` | +| `price_mean` | The arithmetic mean of all prices in the period | `15.1` | +| `interval_count` | Number of price intervals included in the calculation | `96` | ### Usage in Automations & Best Practices @@ -261,16 +264,17 @@ For automations, it is strongly recommended to use the `price_volatility` attrib **Good Example (Robust Automation):** This automation triggers only if the volatility is classified as `high` or `very_high`, respecting your central settings and working independently of the system language. + ```yaml automation: - - alias: "Enable battery optimization only on volatile days" - trigger: - - platform: template - value_template: > - {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }} - action: - - service: input_boolean.turn_on - entity_id: input_boolean.battery_optimization_enabled + - alias: "Enable battery optimization only on volatile days" + trigger: + - platform: template + value_template: > + {{ state_attr('sensor._today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }} + action: + - service: input_boolean.turn_on + entity_id: input_boolean.battery_optimization_enabled ``` --- @@ -283,20 +287,21 @@ You might be tempted to use the numeric `price_coefficient_variation_%` attribut **Bad Example (Brittle Automation):** This automation uses a hard-coded value. If you later change the "High" threshold in the integration's options to 35%, this automation will not respect that change and might trigger at the wrong time. + ```yaml automation: - - alias: "Brittle - Enable battery optimization" - trigger: - # - # BAD: Avoid hard-coding numeric values - # - - platform: numeric_state - entity_id: sensor._today_s_price_volatility - attribute: price_coefficient_variation_% - above: 30 - action: - - service: input_boolean.turn_on - entity_id: input_boolean.battery_optimization_enabled + - alias: "Brittle - Enable battery optimization" + trigger: + # + # BAD: Avoid hard-coding numeric values + # + - platform: numeric_state + entity_id: sensor._today_s_price_volatility + attribute: price_coefficient_variation_% + above: 30 + action: + - service: input_boolean.turn_on + entity_id: input_boolean.battery_optimization_enabled ``` By following the "Good Example", your automations become simpler, more readable, and much easier to maintain. @@ -315,11 +320,11 @@ difference = ((current_price - trailing_avg) / abs(trailing_avg)) × 100% This percentage is then classified: -| Rating | Condition (default) | Meaning | -|--------|---------------------|---------| -| **LOW** | difference ≤ -10% | Significantly below recent average | -| **NORMAL** | -10% < difference < +10% | Within normal range | -| **HIGH** | difference ≥ +10% | Significantly above recent average | +| Rating | Condition (default) | Meaning | +| ---------- | ------------------------ | ---------------------------------- | +| **LOW** | difference ≤ -10% | Significantly below recent average | +| **NORMAL** | -10% < difference < +10% | Within normal range | +| **HIGH** | difference ≥ +10% | Significantly above recent average | **Hysteresis** (default 2%) prevents flickering: once a rating enters LOW, it must cross -8% (not -10%) to return to NORMAL. This avoids rapid switching at threshold boundaries. @@ -341,29 +346,29 @@ stateDiagram-v2 ### Available Rating Sensors -| Sensor | Scope | Description | -|--------|-------|-------------| -| **Current Price Rating** | Current interval | Rating of the current 15-minute price | -| **Next Price Rating** | Next interval | Rating for the upcoming 15-minute price | -| **Previous Price Rating** | Previous interval | Rating for the past 15-minute price | +| Sensor | Scope | Description | +| ----------------------------- | ------------------ | --------------------------------------- | +| **Current Price Rating** | Current interval | Rating of the current 15-minute price | +| **Next Price Rating** | Next interval | Rating for the upcoming 15-minute price | +| **Previous Price Rating** | Previous interval | Rating for the past 15-minute price | | **Current Hour Price Rating** | Rolling 5-interval | Smoothed rating around the current hour | -| **Next Hour Price Rating** | Rolling 5-interval | Smoothed rating around the next hour | -| **Yesterday's Price Rating** | Calendar day | Aggregated rating for yesterday | -| **Today's Price Rating** | Calendar day | Aggregated rating for today | -| **Tomorrow's Price Rating** | Calendar day | Aggregated rating for tomorrow | +| **Next Hour Price Rating** | Rolling 5-interval | Smoothed rating around the next hour | +| **Yesterday's Price Rating** | Calendar day | Aggregated rating for yesterday | +| **Today's Price Rating** | Calendar day | Aggregated rating for today | +| **Tomorrow's Price Rating** | Calendar day | Aggregated rating for tomorrow | ### Ratings vs Levels The integration provides **two** classification systems that serve different purposes: -| | Ratings | Levels | -|--|---------|--------| -| **Source** | Calculated by integration | Provided by Tibber API | -| **Scale** | 3 levels (LOW, NORMAL, HIGH) | 5 levels (VERY_CHEAP → VERY_EXPENSIVE) | -| **Basis** | Trailing 24h average | Daily min/max range | -| **Best for** | Automations (simple thresholds) | Dashboard displays (fine granularity) | -| **Configurable** | Yes (thresholds) | Gap tolerance only | -| **Automation attribute** | `rating_level` (always lowercase English) | `level` (always uppercase English) | +| | Ratings | Levels | +| ------------------------ | ----------------------------------------- | -------------------------------------- | +| **Source** | Calculated by integration | Provided by Tibber API | +| **Scale** | 3 levels (LOW, NORMAL, HIGH) | 5 levels (VERY_CHEAP → VERY_EXPENSIVE) | +| **Basis** | Trailing 24h average | Daily min/max range | +| **Best for** | Automations (simple thresholds) | Dashboard displays (fine granularity) | +| **Configurable** | Yes (thresholds) | Gap tolerance only | +| **Automation attribute** | `rating_level` (always lowercase English) | `level` (always uppercase English) | **Which to use?** @@ -373,11 +378,11 @@ The integration provides **two** classification systems that serve different pur ### Key Attributes -| Attribute | Description | Example | -|-----------|-------------|---------| -| `rating_level` | Language-independent rating (always lowercase) | `low` | -| `difference` | Percentage difference from trailing average | `-12.5` | -| `trailing_avg_24h` | The reference average used for classification | `22.3` | +| Attribute | Description | Example | +| ------------------ | ---------------------------------------------- | ------- | +| `rating_level` | Language-independent rating (always lowercase) | `low` | +| `difference` | Percentage difference from trailing average | `-12.5` | +| `trailing_avg_24h` | The reference average used for classification | `22.3` | ### Usage in Automations @@ -411,26 +416,26 @@ See [Configuration](configuration.md#step-3-price-rating-thresholds) for details Level sensors show the **Tibber API's own price classification** with a 5-level scale: -| Level | Meaning | Numeric Value | -|-------|---------|---------------| -| **VERY_CHEAP** | Exceptionally low | -2 | -| **CHEAP** | Below average | -1 | -| **NORMAL** | Typical range | 0 | -| **EXPENSIVE** | Above average | +1 | -| **VERY_EXPENSIVE** | Exceptionally high | +2 | +| Level | Meaning | Numeric Value | +| ------------------ | ------------------ | ------------- | +| **VERY_CHEAP** | Exceptionally low | -2 | +| **CHEAP** | Below average | -1 | +| **NORMAL** | Typical range | 0 | +| **EXPENSIVE** | Above average | +1 | +| **VERY_EXPENSIVE** | Exceptionally high | +2 | ### Available Level Sensors -| Sensor | Scope | -|--------|-------| -| **Current Price Level** | Current interval | -| **Next Price Level** | Next interval | -| **Previous Price Level** | Previous interval | +| Sensor | Scope | +| ---------------------------- | ------------------------- | +| **Current Price Level** | Current interval | +| **Next Price Level** | Next interval | +| **Previous Price Level** | Previous interval | | **Current Hour Price Level** | Rolling 5-interval window | -| **Next Hour Price Level** | Rolling 5-interval window | -| **Yesterday's Price Level** | Calendar day (aggregated) | -| **Today's Price Level** | Calendar day (aggregated) | -| **Tomorrow's Price Level** | Calendar day (aggregated) | +| **Next Hour Price Level** | Rolling 5-interval window | +| **Yesterday's Price Level** | Calendar day (aggregated) | +| **Today's Price Level** | Calendar day (aggregated) | +| **Tomorrow's Price Level** | Calendar day (aggregated) | **Gap tolerance** smoothing is applied to prevent isolated level flickers (e.g., a single NORMAL between two CHEAPs → corrected to CHEAP). Configure in [options flow](configuration.md#step-4-price-level-gap-tolerance). @@ -440,31 +445,31 @@ These sensors show the lowest and highest prices for calendar days and rolling w ### Daily Min/Max -| Sensor | Description | -|--------|-------------| -| **Today's Lowest Price** | Minimum price today (00:00–23:59) | -| **Today's Highest Price** | Maximum price today (00:00–23:59) | -| **Tomorrow's Lowest Price** | Minimum price tomorrow | -| **Tomorrow's Highest Price** | Maximum price tomorrow | +| Sensor | Description | +| ---------------------------- | --------------------------------- | +| **Today's Lowest Price** | Minimum price today (00:00–23:59) | +| **Today's Highest Price** | Maximum price today (00:00–23:59) | +| **Tomorrow's Lowest Price** | Minimum price tomorrow | +| **Tomorrow's Highest Price** | Maximum price tomorrow | ### 24-Hour Rolling Min/Max -| Sensor | Description | -|--------|-------------| -| **Trailing Price Min** | Lowest price in the last 24 hours | +| Sensor | Description | +| ---------------------- | ---------------------------------- | +| **Trailing Price Min** | Lowest price in the last 24 hours | | **Trailing Price Max** | Highest price in the last 24 hours | -| **Leading Price Min** | Lowest price in the next 24 hours | -| **Leading Price Max** | Highest price in the next 24 hours | +| **Leading Price Min** | Lowest price in the next 24 hours | +| **Leading Price Max** | Highest price in the next 24 hours | ### Key Attributes All min/max sensors include: -| Attribute | Description | -|-----------|-------------| -| `timestamp` | When the extreme price occurs/occurred | -| `price_diff_from_daily_min` | Difference from daily minimum | -| `price_diff_from_daily_min_%` | Percentage difference | +| Attribute | Description | +| ----------------------------- | -------------------------------------- | +| `timestamp` | When the extreme price occurs/occurred | +| `price_diff_from_daily_min` | Difference from daily minimum | +| `price_diff_from_daily_min_%` | Percentage difference | ## Energy Price & Tax Breakdown @@ -474,11 +479,11 @@ Many price sensors expose the **raw energy price** (spot price) and the **tax co ### Where These Attributes Appear -| Sensor Group | Attributes | Description | -|---|---|---| -| **Current/Next/Previous Interval Price** | `energy_price`, `tax` | Raw values for that specific 15-minute interval | -| **Today's/Tomorrow's Min/Max Price** | `energy_price`, `tax` | Values from the interval with the extreme price | -| **Today's/Tomorrow's Average Price** | `energy_price_mean`, `energy_price_median`, `tax_mean`, `tax_median` | Mean and median values across all intervals of the day | +| Sensor Group | Attributes | Description | +| ---------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ | +| **Current/Next/Previous Interval Price** | `energy_price`, `tax` | Raw values for that specific 15-minute interval | +| **Today's/Tomorrow's Min/Max Price** | `energy_price`, `tax` | Values from the interval with the extreme price | +| **Today's/Tomorrow's Average Price** | `energy_price_mean`, `energy_price_median`, `tax_mean`, `tax_median` | Mean and median values across all intervals of the day | :::note Transition After Update After updating the integration, the `energy_price` and `tax` attributes will appear gradually as new price data is fetched from the Tibber API. Existing cached intervals (up to ~2 days old) won't have these fields yet — the attributes will simply be absent until fresh data replaces them. No action needed. @@ -498,7 +503,7 @@ automation: trigger: - platform: numeric_state entity_id: sensor.solar_production_power - above: 2000 # Producing more than 2 kW + above: 2000 # Producing more than 2 kW condition: - condition: template value_template: > @@ -508,7 +513,7 @@ automation: {{ energy is not none and energy > (total * 0.4) }} action: - service: switch.turn_off - entity_id: switch.battery_charging # Don't charge battery, export instead + entity_id: switch.battery_charging # Don't charge battery, export instead ``` #### Price Composition Analysis @@ -577,14 +582,14 @@ stateDiagram-v2 For each period type (Best Price and Peak Price): -| Sensor | When Period Active | When No Active Period | -|--------|-------------------|----------------------| -| **End Time** | Current period's end time | Next period's end time | -| **Period Duration** | Current period length (minutes) | Next period length | -| **Remaining Minutes** | Minutes until current period ends | 0 | -| **Progress** | 0–100% through current period | 0 | -| **Next Start Time** | When next-next period starts | When next period starts | -| **Next In Minutes** | Minutes to next-next period | Minutes to next period | +| Sensor | When Period Active | When No Active Period | +| --------------------- | --------------------------------- | ----------------------- | +| **End Time** | Current period's end time | Next period's end time | +| **Period Duration** | Current period length (minutes) | Next period length | +| **Remaining Minutes** | Minutes until current period ends | 0 | +| **Progress** | 0–100% through current period | 0 | +| **Next Start Time** | When next-next period starts | When next period starts | +| **Next In Minutes** | Minutes to next-next period | Minutes to next period | ### Usage Examples @@ -642,32 +647,33 @@ automation: Trend sensors help you understand **whether to act now or wait**. The integration provides two complementary families: - **Price Outlook Sensors (1h–12h):** Compare current price vs. future window average — "Is now cheaper than the next Nh on average?" -- **Price Trajectory Sensors (2h–12h):** Compare first half vs. second half of the window — "Are prices rising or falling *within* the window?" +- **Price Trajectory Sensors (2h–12h):** Compare first half vs. second half of the window — "Are prices rising or falling _within_ the window?" ### Price Outlook Sensors (1h–12h) These sensors compare the **current price** with the **average price** of the next N hours: -| Sensor | Compares Against | -|--------|-----------------| -| **Price Outlook (1h)** | Average of next 1 hour | -| **Price Outlook (2h)** | Average of next 2 hours | -| **Price Outlook (3h)** | Average of next 3 hours | -| **Price Outlook (4h)** | Average of next 4 hours | -| **Price Outlook (5h)** | Average of next 5 hours | -| **Price Outlook (6h)** | Average of next 6 hours | -| **Price Outlook (8h)** | Average of next 8 hours | +| Sensor | Compares Against | +| ----------------------- | ------------------------ | +| **Price Outlook (1h)** | Average of next 1 hour | +| **Price Outlook (2h)** | Average of next 2 hours | +| **Price Outlook (3h)** | Average of next 3 hours | +| **Price Outlook (4h)** | Average of next 4 hours | +| **Price Outlook (5h)** | Average of next 5 hours | +| **Price Outlook (6h)** | Average of next 6 hours | +| **Price Outlook (8h)** | Average of next 8 hours | | **Price Outlook (12h)** | Average of next 12 hours | :::info Same Starting Point — All Outlook Sensors Use Your Current Price All outlook sensors share the **same base: your current 15-minute price**. They differ only in how far ahead they average. The windows **overlap** — the 3h average includes ALL intervals from the 1h and 2h windows, plus one more hour. **This means:** + - `price_outlook_3h` shows "current price vs. average of the **entire** next 3 hours" — **not** "what happens between hour 2 and hour 3" - If 1h shows `falling` but 6h shows `rising`: near-term prices are below your current price, but looking at the full 6h window (which includes expensive evening hours), the overall average is above your current price - Larger windows smooth out short-term fluctuations — a 30-minute price spike affects the 1h average more than the 6h average -**⚠️ At a price minimum, outlook sensors can be misleading!** If you're at the minimum and prices are about to rise, `price_outlook_3h` may still show `strongly_falling` because the cheap minimum pulls the 3h average below your current high price. Use `price_trajectory_3h` to see the direction *within* the window. +**⚠️ At a price minimum, outlook sensors can be misleading!** If you're at the minimum and prices are about to rise, `price_outlook_3h` may still show `strongly_falling` because the cheap minimum pulls the 3h average below your current high price. Use `price_trajectory_3h` to see the direction _within_ the window. ::: **States:** Each sensor has one of five states: @@ -692,65 +698,66 @@ stateDiagram-v2 F --> SF: accelerates ``` -| State | Meaning | `trend_value` | -|-------|---------|---------------| -| `strongly_falling` | Prices will drop significantly | -2 | -| `falling` | Prices will drop | -1 | -| `stable` | Prices staying roughly the same | 0 | -| `rising` | Prices will increase | +1 | -| `strongly_rising` | Prices will increase significantly | +2 | +| State | Meaning | `trend_value` | +| ------------------ | ---------------------------------- | ------------- | +| `strongly_falling` | Prices will drop significantly | -2 | +| `falling` | Prices will drop | -1 | +| `stable` | Prices staying roughly the same | 0 | +| `rising` | Prices will increase | +1 | +| `strongly_rising` | Prices will increase significantly | +2 | **Key attributes:** -| Attribute | Description | Example | -|-----------|-------------|---------| -| `trend_value` | Numeric value for automations (-2 to +2) | `-1` | -| `trend_Nh_%` | Percentage difference from current price | `-12.3` | -| `next_Nh_avg` | Average price in the future window | `18.5` | -| `second_half_Nh_avg` | Average price in later half of window | `16.2` | -| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` | -| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` | -| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` | -| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` | -| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` | +| Attribute | Description | Example | +| ------------------------------ | --------------------------------------------------------------------- | ------- | +| `trend_value` | Numeric value for automations (-2 to +2) | `-1` | +| `trend_Nh_%` | Percentage difference from current price | `-12.3` | +| `next_Nh_avg` | Average price in the future window | `18.5` | +| `second_half_Nh_avg` | Average price in later half of window | `16.2` | +| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` | +| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` | +| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` | +| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` | +| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` | **Tip:** The `trend_value` attribute (`-2` to `+2`) is ideal for automations — use numeric comparisons instead of matching translated state strings. ### Price Trajectory Sensors (2h–12h) -These sensors compare the **first half** of the future window against the **second half** — revealing the price *direction within* the window. +These sensors compare the **first half** of the future window against the **second half** — revealing the price _direction within_ the window. -| Sensor | Compares | -|--------|----------| -| **Price Trajectory (2h)** | Avg of hour 1 vs avg of hour 2 | -| **Price Trajectory (3h)** | Avg of first 1.5h vs avg of second 1.5h | -| **Price Trajectory (4h)** | Avg of first 2h vs avg of second 2h | -| **Price Trajectory (5h)** | Avg of first 2.5h vs avg of second 2.5h | -| **Price Trajectory (6h)** | Avg of first 3h vs avg of second 3h | -| **Price Trajectory (8h)** | Avg of first 4h vs avg of second 4h | -| **Price Trajectory (12h)** | Avg of first 6h vs avg of second 6h | +| Sensor | Compares | +| -------------------------- | --------------------------------------- | +| **Price Trajectory (2h)** | Avg of hour 1 vs avg of hour 2 | +| **Price Trajectory (3h)** | Avg of first 1.5h vs avg of second 1.5h | +| **Price Trajectory (4h)** | Avg of first 2h vs avg of second 2h | +| **Price Trajectory (5h)** | Avg of first 2.5h vs avg of second 2.5h | +| **Price Trajectory (6h)** | Avg of first 3h vs avg of second 3h | +| **Price Trajectory (8h)** | Avg of first 4h vs avg of second 4h | +| **Price Trajectory (12h)** | Avg of first 6h vs avg of second 6h | **States:** Same 5-level scale as outlook sensors (`strongly_falling` → `strongly_rising`). :::info Why trajectory sensors complement outlook sensors **At a price minimum** — the exact moment you should act — `price_outlook_3h` may show `strongly_falling` because the cheap minimum pulls the entire 3h average below your current high price. But `price_trajectory_3h` shows `rising` because the second half (after the minimum) is more expensive than the first half. -| Combination | Interpretation | -|-------------|----------------| -| Outlook `falling` + Trajectory `rising` | **You're AT the minimum** — act now | -| Outlook `falling` + Trajectory `falling` | Prices still dropping — wait | -| Outlook `rising` + Trajectory `rising` | Strong signal to act now | -| Outlook `rising` + Trajectory `falling` | Short spike, then cheaper — wait | +| Combination | Interpretation | +| ---------------------------------------- | ----------------------------------- | +| Outlook `falling` + Trajectory `rising` | **You're AT the minimum** — act now | +| Outlook `falling` + Trajectory `falling` | Prices still dropping — wait | +| Outlook `rising` + Trajectory `rising` | Strong signal to act now | +| Outlook `rising` + Trajectory `falling` | Short spike, then cheaper — wait | + ::: **Key attributes:** -| Attribute | Description | Example | -|-----------|-------------|---------| -| `trend_value` | Numeric value for automations (-2 to +2) | `1` | -| `first_half_avg` | Average price in first half of window | `12.4` | -| `second_half_avg` | Average price in second half of window | `18.1` | -| `half_diff_%` | Percentage difference (second vs first half) | `46.0` | +| Attribute | Description | Example | +| ----------------- | -------------------------------------------- | ------- | +| `trend_value` | Numeric value for automations (-2 to +2) | `1` | +| `first_half_avg` | Average price in first half of window | `12.4` | +| `second_half_avg` | Average price in second half of window | `18.1` | +| `half_diff_%` | Percentage difference (second vs first half) | `46.0` | ### Current Price Trend @@ -764,11 +771,11 @@ Unlike the simple trend sensors that always compare current price vs future aver **Key attributes:** -| Attribute | Description | Example | -|-----------|-------------|---------| -| `previous_direction` | Price direction before the current trend started | `falling` | -| `price_direction_duration_minutes` | How long prices have been moving in this direction | `45` | -| `price_direction_since` | Timestamp when prices started moving in this direction | `2025-11-08T14:00:00+01:00` | +| Attribute | Description | Example | +| ---------------------------------- | ------------------------------------------------------ | --------------------------- | +| `previous_direction` | Price direction before the current trend started | `falling` | +| `price_direction_duration_minutes` | How long prices have been moving in this direction | `45` | +| `price_direction_since` | Timestamp when prices started moving in this direction | `2025-11-08T14:00:00+01:00` | ### Next Price Trend Change @@ -778,11 +785,11 @@ This sensor predicts **when the current trend will change** by scanning future i **Important:** Only **direction changes** count as trend changes. The five states are grouped into three directions: -| Direction | States | -|-----------|--------| +| Direction | States | +| ----------- | ----------------------------- | | **falling** | `strongly_falling`, `falling` | -| **stable** | `stable` | -| **rising** | `rising`, `strongly_rising` | +| **stable** | `stable` | +| **rising** | `rising`, `strongly_rising` | A change from `rising` to `strongly_rising` (same direction) is **not** reported as a trend change — only actual reversals like `rising` → `stable` or `falling` → `rising`. @@ -790,18 +797,18 @@ A change from `rising` to `strongly_rising` (same direction) is **not** reported **Key attributes:** -| Attribute | Description | Example | -|-----------|-------------|---------| -| `direction` | What the trend will change TO | `rising` | -| `from_direction` | Current trend (will change FROM) | `falling` | -| `minutes_until_change` | Minutes until trend changes | `90` | -| `price_at_change` | Price at the change point | `13.8` | -| `price_avg_after_change` | Average price after change | `18.1` | -| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` | -| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` | -| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` | -| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` | -| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` | +| Attribute | Description | Example | +| ------------------------------ | --------------------------------------------------------------------- | --------- | +| `direction` | What the trend will change TO | `rising` | +| `from_direction` | Current trend (will change FROM) | `falling` | +| `minutes_until_change` | Minutes until trend changes | `90` | +| `price_at_change` | Price at the change point | `13.8` | +| `price_avg_after_change` | Average price after change | `18.1` | +| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` | +| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` | +| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` | +| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` | +| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` | ### How to Use Trend Sensors for Decisions @@ -814,13 +821,13 @@ A natural intuition is to treat trend states like a stock ticker: **This is wrong.** Trend sensors don't show a trajectory — they show a **comparison** between your current price and future prices. The correct interpretation is the opposite: -| State | What the Sensor Calculates | ✅ Correct Action | -|-------|---------------------------|-------------------| -| `falling` | Current price **higher** than future average | **WAIT** — cheaper prices are coming | -| `strongly_falling` | Current price **much higher** than future average | **DEFINITELY WAIT** — significant savings ahead | -| `stable` | Current price **≈ equal** to future average | **Timing doesn't matter** — start whenever convenient | -| `rising` | Current price **lower** than future average | **ACT NOW** — it only gets more expensive | -| `strongly_rising` | Current price **much lower** than future average | **ACT IMMEDIATELY** — best price right now | +| State | What the Sensor Calculates | ✅ Correct Action | +| ------------------ | ------------------------------------------------- | ----------------------------------------------------- | +| `falling` | Current price **higher** than future average | **WAIT** — cheaper prices are coming | +| `strongly_falling` | Current price **much higher** than future average | **DEFINITELY WAIT** — significant savings ahead | +| `stable` | Current price **≈ equal** to future average | **Timing doesn't matter** — start whenever convenient | +| `rising` | Current price **lower** than future average | **ACT NOW** — it only gets more expensive | +| `strongly_rising` | Current price **much lower** than future average | **ACT IMMEDIATELY** — best price right now | **"Rising" is NOT "too late" — it means NOW is the best time because prices will be higher later.** ::: @@ -832,31 +839,31 @@ For most appliances (dishwasher, washing machine, dryer), a single outlook senso ```yaml # Example: Start dishwasher when prices are favorable trigger: - - platform: state - entity_id: sensor.my_home_price_outlook_3h + - platform: state + entity_id: sensor.my_home_price_outlook_3h condition: - - condition: numeric_state - entity_id: sensor.my_home_price_outlook_3h - attribute: trend_value - # rising (1) or strongly_rising (2) = act now - above: 0 + - condition: numeric_state + entity_id: sensor.my_home_price_outlook_3h + attribute: trend_value + # rising (1) or strongly_rising (2) = act now + above: 0 action: - - service: switch.turn_on - target: - entity_id: switch.dishwasher + - service: switch.turn_on + target: + entity_id: switch.dishwasher ``` #### Combining Multiple Windows When short-term and long-term trends disagree, you get richer insight: -| 1h Outlook | 6h Outlook | Interpretation | Recommendation | -|----------|----------|---------------|----------------| -| `rising` | `rising` | Prices going up across the board | **Start now** | -| `falling` | `falling` | Prices dropping across the board | **Wait** | -| `falling` | `rising` | Brief dip, then expensive evening | **Wait briefly**, then start during the dip | -| `rising` | `falling` | Short spike, but cheaper hours ahead | **Wait** if you can — better prices coming | -| `stable` | any | Short-term doesn't matter | Use the **longer window** for your decision | +| 1h Outlook | 6h Outlook | Interpretation | Recommendation | +| ---------- | ---------- | ------------------------------------ | ------------------------------------------- | +| `rising` | `rising` | Prices going up across the board | **Start now** | +| `falling` | `falling` | Prices dropping across the board | **Wait** | +| `falling` | `rising` | Brief dip, then expensive evening | **Wait briefly**, then start during the dip | +| `rising` | `falling` | Short spike, but cheaper hours ahead | **Wait** if you can — better prices coming | +| `stable` | any | Short-term doesn't matter | Use the **longer window** for your decision | #### Dashboard Quick-Glance @@ -870,12 +877,12 @@ On your dashboard, trend sensors give an instant overview: Both sensor families provide future price information, but serve different purposes: -| | Outlook/Trajectory Sensors | Average Sensors | -|--|---------------------------|-----------------| -| **Purpose** | Dashboard display, quick visual overview | Automations, precise numeric comparisons | -| **Output** | Classification (falling/stable/rising) | Exact price values (ct/kWh) | -| **Best for** | "Should I worry about prices?" | "Is the future average below 15 ct?" | -| **Use in** | Dashboard icons, status displays | Template conditions, numeric thresholds | +| | Outlook/Trajectory Sensors | Average Sensors | +| ------------ | ---------------------------------------- | ---------------------------------------- | +| **Purpose** | Dashboard display, quick visual overview | Automations, precise numeric comparisons | +| **Output** | Classification (falling/stable/rising) | Exact price values (ct/kWh) | +| **Best for** | "Should I worry about prices?" | "Is the future average below 15 ct?" | +| **Use in** | Dashboard icons, status displays | Template conditions, numeric thresholds | **Design principle:** Use **trend sensors** (enum) for visual feedback at a glance, use **average sensors** (numeric) for precise decision-making in automations. @@ -902,19 +909,19 @@ This diagnostic sensor provides essential chart configuration values as sensor a **Key Features:** -- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data -- **Automatic Updates**: Refreshes when price data changes (coordinator updates) -- **Lightweight**: Metadata-only mode (no data processing) for fast response -- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) +- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data +- **Automatic Updates**: Refreshes when price data changes (coordinator updates) +- **Lightweight**: Metadata-only mode (no data processing) for fast response +- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed) **Attributes:** -- **`timestamp`**: When the metadata was last fetched -- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) -- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) -- **`currency`**: Currency code (e.g., "EUR", "NOK") -- **`resolution`**: Interval duration in minutes (usually 15) -- **`error`**: Error message if service call failed +- **`timestamp`**: When the metadata was last fetched +- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling) +- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling) +- **`currency`**: Currency code (e.g., "EUR", "NOK") +- **`resolution`**: Interval duration in minutes (usually 15) +- **`error`**: Error message if service call failed **Usage:** @@ -935,24 +942,24 @@ This diagnostic sensor provides cached chart-friendly price data that can be con **Key Features:** -- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) -- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) -- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access -- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) +- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7) +- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes) +- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access +- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed) **Important Notes:** -- ⚠️ Disabled by default - must be manually enabled in entity settings -- ⚠️ Consider using the service instead for better control and flexibility -- ⚠️ Configuration updates require HA restart +- ⚠️ Disabled by default - must be manually enabled in entity settings +- ⚠️ Consider using the service instead for better control and flexibility +- ⚠️ Configuration updates require HA restart **Attributes:** The sensor exposes chart data with metadata in attributes: -- **`timestamp`**: When the data was last fetched -- **`error`**: Error message if service call failed -- **`data`** (or custom name): Array of price data points in configured format +- **`timestamp`**: When the data was last fetched +- **`error`**: Error message if service call failed +- **`data`** (or custom name): Array of price data points in configured format **Configuration:** diff --git a/docs/user/versioned_sidebars/version-v0.21.0-sidebars.json b/docs/user/versioned_sidebars/version-v0.21.0-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.21.0-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.21.0-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_sidebars/version-v0.22.0-sidebars.json b/docs/user/versioned_sidebars/version-v0.22.0-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.22.0-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.22.0-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_sidebars/version-v0.22.1-sidebars.json b/docs/user/versioned_sidebars/version-v0.22.1-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.22.1-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.22.1-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_sidebars/version-v0.23.0-sidebars.json b/docs/user/versioned_sidebars/version-v0.23.0-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.23.0-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.23.0-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_sidebars/version-v0.23.1-sidebars.json b/docs/user/versioned_sidebars/version-v0.23.1-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.23.1-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.23.1-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_sidebars/version-v0.24.0-sidebars.json b/docs/user/versioned_sidebars/version-v0.24.0-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.24.0-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.24.0-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_sidebars/version-v0.27.0-sidebars.json b/docs/user/versioned_sidebars/version-v0.27.0-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.27.0-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.27.0-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_sidebars/version-v0.28.0-sidebars.json b/docs/user/versioned_sidebars/version-v0.28.0-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.28.0-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.28.0-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false } diff --git a/docs/user/versioned_sidebars/version-v0.29.0-sidebars.json b/docs/user/versioned_sidebars/version-v0.29.0-sidebars.json index 2663267..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.29.0-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.29.0-sidebars.json @@ -1,67 +1,53 @@ { - "tutorialSidebar": [ - "intro", - { - "type": "category", - "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "📊 Features", - "items": [ - "sensors", - "period-calculation", - "dynamic-icons", - "icon-colors", - "actions" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "🤖 Automation", - "items": [ - "automation-examples" - ], - "collapsible": true, - "collapsed": false - }, - { - "type": "category", - "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], - "collapsible": true, - "collapsed": false - } - ] + "tutorialSidebar": [ + "intro", + { + "type": "category", + "label": "🚀 Getting Started", + "items": ["installation", "configuration"], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "📖 Core Concepts", + "items": ["concepts", "glossary"], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "📊 Features", + "items": [ + "sensors", + "period-calculation", + "dynamic-icons", + "icon-colors", + "actions" + ], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "🎨 Visualization", + "items": ["dashboard-examples", "chart-examples"], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "🤖 Automation", + "items": ["automation-examples"], + "collapsible": true, + "collapsed": false + }, + { + "type": "category", + "label": "🔧 Help & Support", + "items": ["faq", "troubleshooting"], + "collapsible": true, + "collapsed": false + } + ] } diff --git a/docs/user/versioned_sidebars/version-v0.30.0-sidebars.json b/docs/user/versioned_sidebars/version-v0.30.0-sidebars.json index 6938399..215e2c4 100644 --- a/docs/user/versioned_sidebars/version-v0.30.0-sidebars.json +++ b/docs/user/versioned_sidebars/version-v0.30.0-sidebars.json @@ -4,20 +4,14 @@ { "type": "category", "label": "🚀 Getting Started", - "items": [ - "installation", - "configuration" - ], + "items": ["installation", "configuration"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "📖 Core Concepts", - "items": [ - "concepts", - "glossary" - ], + "items": ["concepts", "glossary"], "collapsible": true, "collapsed": false }, @@ -37,29 +31,21 @@ { "type": "category", "label": "🎨 Visualization", - "items": [ - "dashboard-examples", - "chart-examples" - ], + "items": ["dashboard-examples", "chart-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🤖 Automation", - "items": [ - "automation-examples" - ], + "items": ["automation-examples"], "collapsible": true, "collapsed": false }, { "type": "category", "label": "🔧 Help & Support", - "items": [ - "faq", - "troubleshooting" - ], + "items": ["faq", "troubleshooting"], "collapsible": true, "collapsed": false }