Compare commits

...

30 commits

Author SHA1 Message Date
Julian Pawlowski
aa0f543ec5 fix: ensure SVG backgrounds are solid to prevent transparency issues
Some checks failed
Lint / Ruff (push) Waiting to run
Validate / Hassfest validation (push) Waiting to run
Validate / HACS validation (push) Waiting to run
Auto-Tag on Version Bump / Check and create version tag (push) Has been cancelled
Deploy Docusaurus Documentation (Dual Sites) / Build and Deploy Documentation Sites (push) Has been cancelled
2026-04-07 15:32:52 +00:00
Julian Pawlowski
a4a43e3d34 fix: update default Copilot model version to 4.6 2026-04-07 15:22:29 +00:00
Julian Pawlowski
4efd6b7267 feat: add versioned sidebars for user and developer documentation 2026-04-07 15:19:10 +00:00
Julian Pawlowski
bb176135f6 fix(ci): ensure release notes end with newline before heredoc delimiter
cliff.toml has trim=true which strips git-cliff's trailing newline.
When written to GITHUB_OUTPUT via heredoc, the closing delimiter was
appended to the last content line instead of its own line, causing
"Matching delimiter not found" error.

Added printf '\n' in the workflow and echo "" in generate-notes to
guarantee a newline before the heredoc closing delimiter.

Impact: Release workflow no longer fails when generating release notes.
2026-04-07 15:16:03 +00:00
Julian Pawlowski
c160063067 Merge branch 'main' of https://github.com/jpawlowski/hass.tibber_prices 2026-04-07 15:09:58 +00:00
Julian Pawlowski
8bd3ef85c0 fix(ci): use random heredoc delimiter in release workflow
$GITHUB_OUTPUT heredoc blocks used literal 'EOF' as delimiter, which
breaks parsing if generated release notes contain 'EOF' on its own line.

Replace static 'EOF' with openssl rand -hex 16 random delimiter in
both the version-warning and release-notes output blocks.

Impact: Release workflow no longer fails when commit message bodies
contain 'EOF'.
2026-04-07 15:09:56 +00:00
github-actions[bot]
48d088281b docs: add version snapshot v0.29.0 and cleanup old versions [skip ci] 2026-04-07 15:09:08 +00:00
Julian Pawlowski
070905e880 chore(release): bump version to 0.29.0 2026-04-07 15:07:08 +00:00
Julian Pawlowski
b7cf4442bd feat: enhance mermaid lightbox interaction with improved accessibility and hover hints 2026-04-07 15:06:23 +00:00
Julian Pawlowski
dba96e38e0 fix(css): enhance mermaid lightbox styling for better theme integration 2026-04-07 14:58:10 +00:00
Julian Pawlowski
ebc3c38007 Add @docusaurus/faster dependency to package.json 2026-04-07 14:50:57 +00:00
Julian Pawlowski
86c5db179e fix(css): adjust width of mermaid lightbox inner card for better responsiveness 2026-04-07 14:46:51 +00:00
dependabot[bot]
c7bd0b7a93
chore(deps): bump the react group in /docs/developer with 2 updates (#103)
Bumps the react group in /docs/developer with 2 updates: [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom).


Updates `react` from 19.2.1 to 19.2.4
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react)

Updates `react-dom` from 19.2.1 to 19.2.4
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: react-dom
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 16:39:58 +02:00
dependabot[bot]
688cf0d5a3
chore(deps-dev): bump typescript from 5.6.3 to 6.0.2 in /docs/user (#102)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 16:38:53 +02:00
dependabot[bot]
de577c83a6
chore(deps-dev): bump typescript from 5.6.3 to 6.0.2 in /docs/developer (#104)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 16:37:15 +02:00
dependabot[bot]
fd3c949e90
chore(deps): bump the react group in /docs/user with 2 updates (#101)
Bumps the react group in /docs/user with 2 updates: [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom).


Updates `react` from 19.2.1 to 19.2.4
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react)

Updates `react-dom` from 19.2.1 to 19.2.4
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: react-dom
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 16:36:34 +02:00
dependabot[bot]
fc8ec33f11
chore(deps): bump home-assistant/actions (#100)
Bumps [home-assistant/actions](https://github.com/home-assistant/actions) from 5752577ea7cc5aefb064b0b21432f18fe4d6ba90 to f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b.
- [Release notes](https://github.com/home-assistant/actions/releases)
- [Commits](5752577ea7...f6f29a7ee3)

---
updated-dependencies:
- dependency-name: home-assistant/actions
  dependency-version: f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 16:36:07 +02:00
Julian Pawlowski
f4fecc3ee0 feat(deps): add npm package updates for user and developer documentation directories 2026-04-07 14:32:12 +00:00
Julian Pawlowski
5f52dd2524 chore: update dependencies to version 3.10.0 in package.json 2026-04-07 14:31:25 +00:00
Julian Pawlowski
db900c2a4b fix(docs): update links in automation examples and intro documentation 2026-04-07 14:25:30 +00:00
Julian Pawlowski
3aa8a43e3a fix(docs): commit missing versioned_sidebars for all existing versions
versioned_docs/ was already tracked but versioned_sidebars/ was never
committed. Docusaurus requires both to render sidebar navigation for
old versions — without the sidebar files, all versioned pages show
no navigation.

Adds sidebar snapshots for user and developer docs:
  v0.21.0, v0.22.0, v0.22.1, v0.23.0, v0.23.1, v0.24.0, v0.27.0, v0.28.0

Future versions: CI (docusaurus.yml) runs docs:version on each stable
tag push, which generates both versioned_docs/ and versioned_sidebars/.
The workflow should be updated to commit these files back, or they need
to be added manually after each release.

Impact: Sidebar navigation now appears correctly for all existing
versioned documentation pages.
2026-04-07 14:16:53 +00:00
Julian Pawlowski
9fea34b8b4 fix(deps): remove unnecessary peer dependencies from package-lock.json 2026-04-07 14:09:27 +00:00
Julian Pawlowski
552db6ef7d feat(docs): add click-to-zoom lightbox for Mermaid diagrams
Swizzled @docusaurus/theme-mermaid's Mermaid component to wrap
every diagram with a portal-based lightbox overlay.

An expand icon appears on diagram hover. Clicking opens the SVG
in a full-screen overlay (90vw × 85vh, scrollable). Closes via
backdrop click, Escape key, or close button. SSR-safe, no
external dependencies. Matches Tibber electric color theme.

Impact: Users can inspect complex flowcharts (e.g. the Options
Flow wizard) without squinting at small embedded diagrams.
2026-04-07 14:09:19 +00:00
Julian Pawlowski
1c25ac1fb0 feat(docs): improve Giscus comment UX for maintainer clarity
Switched Giscus mapping from 'pathname' to 'og:title' with strict=1.
Discussions are now titled after the readable page title (e.g.
'Chart Examples | Tibber Prices Integration') instead of the URL path,
making them immediately identifiable in the GitHub Discussions list.

Added a small hint above every comment box pointing users to open
a dedicated Discussion on GitHub for new questions/ideas, so
page-specific comments don't accumulate unrelated threads.

Note: Existing Discussion #94 (pathname-mapped) will no longer appear
on chart-examples — a new og:title-mapped discussion will be created
on the next comment. #94 remains visible on GitHub.

Impact: Maintainer can identify discussion origin at a glance.
Users are guided toward proper Discussion threads for new topics.
2026-04-07 14:01:37 +00:00
Julian Pawlowski
d8f005d3bb docs: explain how to find the config entry_id
Added a dedicated 'Finding Your Entry ID' section to actions.md
explaining the two workflows: dropdown in the Action UI vs.
'Copy Config Entry ID' from the integration's three-dot menu in YAML.

Added matching :::info callouts to chart-examples.md and
automation-examples.md where entry_id: YOUR_ENTRY_ID appears in code
examples, and a new 'Config Entry ID' entry in the glossary.

Addresses user confusion reported in GitHub Discussions #94.

Impact: Users no longer get stuck on YOUR_ENTRY_ID placeholders.
Both GUI and YAML workflows are clearly explained at the point of need.
2026-04-07 14:01:33 +00:00
Julian Pawlowski
190c979e9c docs(user): comprehensive trend decision model and automation guides
sensors.md:
- Added "How to Use Trend Sensors for Decisions" section with :::danger
  "Common Misconception — Don't Wait for Stable!" box
- Comparison table (rising/falling/stable → what it means → action)
- Basic automation YAML pattern
- Multi-window combination table (1h + 6h → interpretation)
- Dashboard quick-glance guide
- Documented trend_change_in_minutes sensor

automation-examples.md:
- Replaced placeholder with full automation examples
- V-shaped price days explanation with mermaid timeline
- "Ride the Full Cheap Wave" heat pump automation
- "Pre-Emptive Start Before Best Price" pattern
- "Protect Against Rising Prices" EV charging automation
- "Multi-Window Trend Strategy" (1h + 6h + best_price_period)
- Sensor Combination Quick Reference table
- Volatility-based automation examples
- Best hour detection automation
- ApexCharts card configuration guide

Other docs: trend-related updates to concepts, configuration,
glossary, installation, period-calculation, troubleshooting.

Impact: Users have comprehensive guides for trend-based automations
with real YAML examples and clear decision frameworks.
2026-04-07 13:45:40 +00:00
Julian Pawlowski
5d673e65b4 feat(translations): add trend sensor descriptions and decision model tips
Standard translations (5 languages):
- Added config flow labels/descriptions for trend change confirmation,
  min price change, and min price change strongly
- Updated strongly threshold descriptions (6% → 9%)
- Added trend_change_in_minutes sensor name

Custom translations (5 languages):
- Rewritten usage_tips for all 8 trend sensors (1h-12h) with action-
  oriented decision guide: "rising = ACT NOW", "falling = WAIT"
- Addresses common misconception ("rising" ≠ "too late")
- Added trend_change_in_minutes description and tips
- Updated long_descriptions: clarified shared-base behavior, corrected
  threshold references from >5% to ±3%/±9%
- Updated next_price_trend_change: direction-group explanation

Impact: Users understand trend sensors as decision tools, not trajectory
indicators. All 5 languages (en/de/nb/nl/sv) updated consistently.
2026-04-07 13:45:13 +00:00
Julian Pawlowski
798de5946d feat(config_flow): add trend confirmation and noise floor settings
Added 3 new config fields to price trend options step:
- Trend Change Confirmation (2-6 intervals slider)
- Min Price Change for trend (display-unit-aware slider)
- Min Price Change for strong trend (display-unit-aware slider)

Price change sliders scale between base currency (EUR/NOK) storage and
display unit (ct/øre) presentation using get_display_unit_factor().
Added migration in __init__.py to convert old display-unit values to
base currency format.

Impact: Users can tune trend sensitivity: higher confirmation = fewer
false changes, higher min price change = no trends from tiny fluctuations.
2026-04-07 13:44:47 +00:00
Julian Pawlowski
91efeed90f feat(sensors): add trend_change_in_minutes countdown sensor
New duration sensor showing time until next price trend change as hours
(e.g., 2.25 h). Registered in MINUTE_UPDATE_ENTITY_KEYS for per-minute
updates. Shares cached attributes with next_price_trend_change timestamp
sensor.

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

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

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

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

Updated tests for new 4-tuple return value.

Impact: More stable trend detection — fewer false trend changes during low-price
periods. Direction-group logic prevents noise from "rising ↔ strongly_rising"
oscillations. Users can fine-tune noise floor for their market.
2026-04-07 13:44:01 +00:00
100 changed files with 56383 additions and 41172 deletions

View file

@ -11,10 +11,36 @@ updates:
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/docs/user"
schedule:
interval: "weekly"
groups:
docusaurus:
patterns:
- "@docusaurus/*"
react:
patterns:
- "react"
- "react-dom"
- package-ecosystem: "npm"
directory: "/docs/developer"
schedule:
interval: "weekly"
groups:
docusaurus:
patterns:
- "@docusaurus/*"
react:
patterns:
- "react"
- "react-dom"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
ignore:
# Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json
- dependency-name: "homeassistant"
- dependency-name: "homeassistant"

View file

@ -137,8 +137,8 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
# Add version files from both docs
git add docs/user/versioned_docs/ docs/user/versions.json 2>/dev/null || true
git add docs/developer/versioned_docs/ docs/developer/versions.json 2>/dev/null || true
git add docs/user/versioned_docs/ docs/user/versioned_sidebars/ docs/user/versions.json 2>/dev/null || true
git add docs/developer/versioned_docs/ docs/developer/versioned_sidebars/ docs/developer/versions.json 2>/dev/null || true
# Commit if there are changes
if git diff --staged --quiet; then

View file

@ -181,8 +181,10 @@ jobs:
echo "Commits analyzed: Breaking=$BREAKING, Features=$FEAT, Fixes=$FIX"
# Set output for later steps (using heredoc for multi-line)
# Use random delimiter to avoid collision if content contains 'EOF'
WARN_DELIM=$(openssl rand -hex 16)
{
echo "warning<<EOF"
echo "warning<<${WARN_DELIM}"
echo "$WARNING"
echo ""
echo "$SUGGESTION"
@ -195,7 +197,7 @@ jobs:
echo "3. Push the corrected tag: \`git push origin v<suggested-version>\`"
echo ""
echo "**This tag will be automatically deleted in the next step.**"
echo "EOF"
echo "${WARN_DELIM}"
} >> $GITHUB_OUTPUT
else
echo "✓ Version bump looks appropriate for the changes"
@ -242,10 +244,13 @@ jobs:
echo "title=$TITLE" >> $GITHUB_OUTPUT
# Output for GitHub Actions
# Use random delimiter to avoid collision if release notes contain 'EOF'
NOTES_DELIM=$(openssl rand -hex 16)
{
echo 'notes<<EOF'
echo "notes<<${NOTES_DELIM}"
cat release-notes.md
echo EOF
printf '\n' # Ensure content ends with newline (git-cliff trim=true removes it)
echo "${NOTES_DELIM}"
} >> $GITHUB_OUTPUT
- name: Create GitHub Release

View file

@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run hassfest validation
uses: home-assistant/actions/hassfest@5752577ea7cc5aefb064b0b21432f18fe4d6ba90 # master
uses: home-assistant/actions/hassfest@f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b # master
hacs: # https://github.com/hacs/action
name: HACS validation

View file

@ -21,11 +21,15 @@ from homeassistant.loader import async_get_loaded_integration
from .api import TibberPricesApiClient
from .const import (
CONF_CURRENCY_DISPLAY_MODE,
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
DATA_CHART_CONFIG,
DATA_CHART_METADATA_CONFIG,
DISPLAY_MODE_SUBUNIT,
DOMAIN,
LOGGER,
MAX_PRICE_TREND_MIN_PRICE_CHANGE,
MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
async_load_standard_translations,
async_load_translations,
)
@ -141,6 +145,29 @@ async def _migrate_config_options(hass: HomeAssistant, entry: ConfigEntry) -> No
DISPLAY_MODE_SUBUNIT,
)
# Migration: Convert min_price_change from display currency (ct/øre) to base currency (EUR/NOK)
# Before this change, values were stored in display units. Now always stored in base currency.
# Detection: If either value exceeds its new max, both are in old format and need conversion.
# Old range: 0-5.0 ct / 0-10.0 ct, New range: 0-0.05 EUR / 0-0.10 EUR
normal_val = migrated.get(CONF_PRICE_TREND_MIN_PRICE_CHANGE)
strongly_val = migrated.get(CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY)
old_format_detected = (normal_val is not None and normal_val > MAX_PRICE_TREND_MIN_PRICE_CHANGE) or (
strongly_val is not None and strongly_val > MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY
)
if old_format_detected:
for key in (CONF_PRICE_TREND_MIN_PRICE_CHANGE, CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY):
if key in migrated and migrated[key] > 0:
old_val = migrated[key]
migrated[key] = round(old_val / 100, 6)
migration_performed = True
LOGGER.info(
"[%s] Migrated config: %s = %s -> %s (converted to base currency)",
entry.title,
key,
old_val,
migrated[key],
)
# Save migrated options if any changes were made
if migration_performed:
hass.config_entries.async_update_entry(entry, options=migrated)

View file

@ -60,6 +60,8 @@ from custom_components.tibber_prices.const import (
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
CONF_PRICE_RATING_THRESHOLD_HIGH,
CONF_PRICE_RATING_THRESHOLD_LOW,
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
CONF_PRICE_TREND_THRESHOLD_FALLING,
CONF_PRICE_TREND_THRESHOLD_RISING,
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
@ -74,7 +76,10 @@ from custom_components.tibber_prices.const import (
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
DOMAIN,
async_get_translation,
format_price_unit_base,
format_price_unit_subunit,
get_default_options,
get_display_unit_factor,
)
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
from homeassistant.helpers import entity_registry as er
@ -730,6 +735,9 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
"""Configure price trend thresholds."""
errors: dict[str, str] = {}
# Get display factor for currency conversion
display_factor = get_display_unit_factor(self.config_entry)
if user_input is not None:
# Schema is now flattened - fields come directly in user_input
# Store them flat in options (no nested structure)
@ -775,6 +783,15 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
)
if not errors:
# Convert min_price_change values from display unit to base currency for storage
# (dividing by 1 is a no-op for base currency mode)
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE] = round(
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE] / display_factor, 6
)
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY] = round(
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY] / display_factor, 6
)
# Store flat data directly in options (no section wrapping)
self._options.update(user_input)
# async_create_entry automatically handles change detection and listener triggering
@ -782,9 +799,20 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
# Return to menu for more changes
return await self.async_step_init()
# Get currency code for unit label on sliders
currency_code = self.config_entry.data.get("currency", None)
if display_factor > 1:
price_unit = format_price_unit_subunit(currency_code)
else:
price_unit = format_price_unit_base(currency_code)
return self.async_show_form(
step_id="price_trend",
data_schema=get_price_trend_schema(self.config_entry.options),
data_schema=get_price_trend_schema(
self.config_entry.options,
display_factor=display_factor,
price_unit=price_unit,
),
errors=errors,
description_placeholders=self._get_entity_warning_placeholders("price_trend"),
)

View file

@ -33,6 +33,9 @@ from custom_components.tibber_prices.const import (
CONF_PRICE_RATING_HYSTERESIS,
CONF_PRICE_RATING_THRESHOLD_HIGH,
CONF_PRICE_RATING_THRESHOLD_LOW,
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
CONF_PRICE_TREND_THRESHOLD_FALLING,
CONF_PRICE_TREND_THRESHOLD_RISING,
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
@ -66,6 +69,9 @@ from custom_components.tibber_prices.const import (
DEFAULT_PRICE_RATING_HYSTERESIS,
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
DEFAULT_PRICE_TREND_CHANGE_CONFIRMATION,
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE,
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
@ -88,7 +94,10 @@ from custom_components.tibber_prices.const import (
MAX_PRICE_RATING_HYSTERESIS,
MAX_PRICE_RATING_THRESHOLD_HIGH,
MAX_PRICE_RATING_THRESHOLD_LOW,
MAX_PRICE_TREND_CHANGE_CONFIRMATION,
MAX_PRICE_TREND_FALLING,
MAX_PRICE_TREND_MIN_PRICE_CHANGE,
MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
MAX_PRICE_TREND_RISING,
MAX_PRICE_TREND_STRONGLY_FALLING,
MAX_PRICE_TREND_STRONGLY_RISING,
@ -103,7 +112,10 @@ from custom_components.tibber_prices.const import (
MIN_PRICE_RATING_HYSTERESIS,
MIN_PRICE_RATING_THRESHOLD_HIGH,
MIN_PRICE_RATING_THRESHOLD_LOW,
MIN_PRICE_TREND_CHANGE_CONFIRMATION,
MIN_PRICE_TREND_FALLING,
MIN_PRICE_TREND_MIN_PRICE_CHANGE,
MIN_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
MIN_PRICE_TREND_RISING,
MIN_PRICE_TREND_STRONGLY_FALLING,
MIN_PRICE_TREND_STRONGLY_RISING,
@ -907,8 +919,16 @@ def get_peak_price_schema(
)
def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
def get_price_trend_schema(
options: Mapping[str, Any],
*,
display_factor: int = 1,
price_unit: str = "",
) -> vol.Schema:
"""Return schema for price trend thresholds configuration."""
# Scale min_price_change values for display (stored in base currency, shown in display unit)
step = 0.1 if display_factor > 1 else 0.001
return vol.Schema(
{
vol.Optional(
@ -979,6 +999,64 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
default=int(
options.get(
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
DEFAULT_PRICE_TREND_CHANGE_CONFIRMATION,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_PRICE_TREND_CHANGE_CONFIRMATION,
max=MAX_PRICE_TREND_CHANGE_CONFIRMATION,
step=1,
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
default=round(
float(
options.get(
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE,
)
)
* display_factor,
3,
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_PRICE_TREND_MIN_PRICE_CHANGE * display_factor,
max=MAX_PRICE_TREND_MIN_PRICE_CHANGE * display_factor,
step=step,
unit_of_measurement=price_unit,
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
default=round(
float(
options.get(
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
)
)
* display_factor,
3,
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY * display_factor,
max=MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY * display_factor,
step=step,
unit_of_measurement=price_unit,
mode=NumberSelectorMode.SLIDER,
),
),
}
)

View file

@ -52,6 +52,9 @@ CONF_PRICE_TREND_THRESHOLD_RISING = "price_trend_threshold_rising"
CONF_PRICE_TREND_THRESHOLD_FALLING = "price_trend_threshold_falling"
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING = "price_trend_threshold_strongly_rising"
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING = "price_trend_threshold_strongly_falling"
CONF_PRICE_TREND_CHANGE_CONFIRMATION = "price_trend_change_confirmation"
CONF_PRICE_TREND_MIN_PRICE_CHANGE = "price_trend_min_price_change"
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY = "price_trend_min_price_change_strongly"
CONF_VOLATILITY_THRESHOLD_MODERATE = "volatility_threshold_moderate"
CONF_VOLATILITY_THRESHOLD_HIGH = "volatility_threshold_high"
CONF_VOLATILITY_THRESHOLD_VERY_HIGH = "volatility_threshold_very_high"
@ -103,10 +106,15 @@ DEFAULT_PRICE_LEVEL_GAP_TOLERANCE = 1 # Max consecutive intervals to smooth out
DEFAULT_AVERAGE_SENSOR_DISPLAY = "median" # Default: show median in state, mean in attributes
DEFAULT_PRICE_TREND_THRESHOLD_RISING = 3 # Default trend threshold for rising prices (%)
DEFAULT_PRICE_TREND_THRESHOLD_FALLING = -3 # Default trend threshold for falling prices (%, negative value)
# Strong trend thresholds default to 2x the base threshold.
# These are independently configurable to allow fine-tuning of "strongly" detection.
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING = 6 # Default strong rising threshold (%)
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING = -6 # Default strong falling threshold (%, negative value)
# Strong trend thresholds default to 3x the base threshold for perceptual scaling.
# The non-linear ratio (3% → 9%) ensures "strongly" feels significantly different from "rising/falling".
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING = 9 # Default strong rising threshold (%)
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING = -9 # Default strong falling threshold (%, negative value)
DEFAULT_PRICE_TREND_CHANGE_CONFIRMATION = 3 # Default consecutive intervals to confirm trend change (3 x 15min = 45min)
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE = 0.005 # Minimum absolute price change for trend (in base currency, e.g. EUR/NOK)
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY = (
0.015 # Minimum absolute price change for strong trend (in base currency)
)
# Default volatility thresholds (relative values using coefficient of variation)
# Coefficient of variation = (standard_deviation / mean) * 100%
# These thresholds are unitless and work across different price levels
@ -172,6 +180,14 @@ MIN_PRICE_TREND_STRONGLY_RISING = 2 # Minimum strongly rising threshold (must b
MAX_PRICE_TREND_STRONGLY_RISING = 100 # Maximum strongly rising threshold
MIN_PRICE_TREND_STRONGLY_FALLING = -100 # Minimum strongly falling threshold (negative)
MAX_PRICE_TREND_STRONGLY_FALLING = -2 # Maximum strongly falling threshold (must be < falling)
# Trend change confirmation limits (consecutive 15-min intervals)
MIN_PRICE_TREND_CHANGE_CONFIRMATION = 2 # Minimum: 2 intervals (30 min) - fast but more noise
MAX_PRICE_TREND_CHANGE_CONFIRMATION = 6 # Maximum: 6 intervals (90 min) - very stable but slow
# Minimum absolute price change thresholds (noise floor, in base currency e.g. EUR/NOK)
MIN_PRICE_TREND_MIN_PRICE_CHANGE = 0.0 # 0 = disabled (pure percentage mode)
MAX_PRICE_TREND_MIN_PRICE_CHANGE = 0.05 # 5 ct / 5 øre equivalent
MIN_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY = 0.0 # 0 = disabled
MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY = 0.10 # 10 ct / 10 øre equivalent
# Gap count and relaxation limits
MIN_GAP_COUNT = 0 # Minimum gap count
@ -362,6 +378,11 @@ def get_default_options(currency_code: str | None) -> dict[str, Any]:
# Price trend thresholds (flat - single-section step)
CONF_PRICE_TREND_THRESHOLD_RISING: DEFAULT_PRICE_TREND_THRESHOLD_RISING,
CONF_PRICE_TREND_THRESHOLD_FALLING: DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING: DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING: DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
CONF_PRICE_TREND_CHANGE_CONFIRMATION: DEFAULT_PRICE_TREND_CHANGE_CONFIRMATION,
CONF_PRICE_TREND_MIN_PRICE_CHANGE: DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE,
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY: DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
# Nested section: Period settings (shared by best/peak price)
"period_settings": {
CONF_BEST_PRICE_MIN_PERIOD_LENGTH: DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,

View file

@ -108,5 +108,7 @@ MINUTE_UPDATE_ENTITY_KEYS = frozenset(
"peak_price_remaining_minutes",
"peak_price_progress",
"peak_price_next_in_minutes",
# Trend change countdown sensor (needs minute updates)
"trend_change_in_minutes",
}
)

View file

@ -229,43 +229,43 @@
},
"price_trend_1h": {
"description": "Preistrend für die nächste Stunde",
"long_description": "Vergleicht aktuellen Intervallpreis mit Durchschnitt der nächsten 1 Stunde (4 Intervalle). Steigend wenn Zukunft >5% höher, fallend wenn >5% niedriger, sonst stabil.",
"usage_tips": "Relative Optimierung: 'fallend' = warten, Preise sinken. 'steigend' = jetzt handeln oder du zahlst mehr. 'stabil' = Preis spielt gerade keine große Rolle. Funktioniert unabhängig vom absoluten Preisniveau."
"long_description": "Vergleicht deinen aktuellen Preis mit dem Durchschnitt aller Intervalle der nächsten Stunde (4 Intervalle). Alle Trend-Sensoren (1h12h) haben dieselbe Basis: dein aktueller Preis — sie unterscheiden sich nur im Zeitfenster. Größere Fenster umfassen mehr Zukunftsstunden und glätten kurzfristige Spitzen. Steigend/fallend ab ±3%, stark ab ±9% (konfigurierbar, volatilitätsadaptiv).",
"usage_tips": "Entscheidungshilfe: 'steigend' = JETZT HANDELN, dein aktueller Preis ist günstiger als die nächste Stunde. 'fallend' = WARTEN, günstigere Preise kommen. 'stabil' = Timing egal. Häufiges Missverständnis: 'steigend' bedeutet NICHT 'zu spät' — es heißt, jetzt ist gerade ein guter Preis! Funktioniert unabhängig vom absoluten Preisniveau."
},
"price_trend_2h": {
"description": "Preistrend für die nächsten 2 Stunden",
"long_description": "Vergleicht aktuellen Intervallpreis mit Durchschnitt der nächsten 2 Stunden (8 Intervalle). Steigend wenn Zukunft >5% höher, fallend wenn >5% niedriger, sonst stabil.",
"usage_tips": "Relative Optimierung: Ideal für Haushaltsgeräte. 'fallend' bedeutet bessere Preise kommen in 2h - verschiebe wenn möglich. Findet bestes Timing in deinem verfügbaren Zeitfenster, unabhängig von der Saison."
"long_description": "Vergleicht deinen aktuellen Preis mit dem Durchschnitt aller Intervalle der nächsten 2 Stunden (8 Intervalle) — das gesamte Zeitfenster ab jetzt, nicht nur der spätere Teil. Steigend/fallend ab ±3%, stark ab ±9% (konfigurierbar, volatilitätsadaptiv).",
"usage_tips": "Haushaltsgeräte: 'steigend' = jetzt starten, du hast gerade einen guten Preis. 'fallend' = bessere Preise kommen in 2h, verschiebe wenn möglich. 'stabil' = egal, starte nach Bedarf. Nicht auf 'stabil' warten — bei 'steigend' ist JETZT der beste Zeitpunkt."
},
"price_trend_3h": {
"description": "Preistrend für die nächsten 3 Stunden",
"long_description": "Vergleicht aktuellen Intervallpreis mit Durchschnitt der nächsten 3 Stunden (12 Intervalle). Steigend wenn Zukunft >5% höher, fallend wenn >5% niedriger, sonst stabil.",
"usage_tips": "Relative Optimierung: Für Eco-Programme. 'fallend' bedeutet Preise sinken >5% - lohnt sich zu warten. Funktioniert in jeder Saison. Kombiniere mit avg-Sensor für Preisobergrenze: nur wenn avg < dein Limit UND Trend nicht 'fallend'."
"long_description": "Vergleicht deinen aktuellen Preis mit dem Durchschnitt aller Intervalle der nächsten 3 Stunden (12 Intervalle) — das gesamte Zeitfenster ab jetzt, nicht nur der spätere Teil. Steigend/fallend ab ±3%, stark ab ±9% (konfigurierbar, volatilitätsadaptiv).",
"usage_tips": "Eco-Programme: 'steigend' = Eco-Zyklus jetzt starten, Preise steigen ab hier. 'fallend' = warten, günstigeres Fenster kommt. Kombiniere mit avg-Sensor: starte wenn Trend 'steigend' oder 'stabil' UND avg < dein Limit. Funktioniert in jeder Saison."
},
"price_trend_4h": {
"description": "Preistrend für die nächsten 4 Stunden",
"long_description": "Vergleicht aktuellen Intervallpreis mit Durchschnitt der nächsten 4 Stunden (16 Intervalle). Steigend wenn Zukunft >5% höher, fallend wenn >5% niedriger, sonst stabil.",
"usage_tips": "Relative Optimierung: Wärmepumpen/Batterie-Entscheidungen. 'fallend' bedeutet besseres Ladefenster kommt. Findet immer relative beste Zeit - ob Preise 10 Cent oder 50 Cent sind. Nutze avg-Sensor für absolute Grenze."
"long_description": "Vergleicht deinen aktuellen Preis mit dem Durchschnitt aller Intervalle der nächsten 4 Stunden (16 Intervalle) — das gesamte Zeitfenster ab jetzt, nicht nur der spätere Teil. Steigend/fallend ab ±3%, stark ab ±9% (konfigurierbar, volatilitätsadaptiv).",
"usage_tips": "Wärmepumpe/Batterie: 'steigend' = jetzt laden, du bist an einem relativen Tiefpunkt. 'fallend' = warten auf besseres Ladefenster. 'stabil' = laden nach Bedarf. Funktioniert unabhängig vom Preisniveau — findet relative beste Zeit ob 10 oder 50 Cent."
},
"price_trend_5h": {
"description": "Preistrend für die nächsten 5 Stunden",
"long_description": "Vergleicht aktuellen Intervallpreis mit Durchschnitt der nächsten 5 Stunden (20 Intervalle). Steigend wenn Zukunft >5% höher, fallend wenn >5% niedriger, sonst stabil.",
"usage_tips": "Relative Optimierung: Erweiterte Betriebszyklen. Passt sich dem Markt an - findet bestes relatives Timing in jedem Preisumfeld. 'stabil/steigend' = guter Zeitpunkt zum Starten in deinem Planungsfenster."
"long_description": "Vergleicht deinen aktuellen Preis mit dem Durchschnitt aller Intervalle der nächsten 5 Stunden (20 Intervalle) — das gesamte Zeitfenster ab jetzt, nicht nur der spätere Teil. Steigend/fallend ab ±3%, stark ab ±9% (konfigurierbar, volatilitätsadaptiv).",
"usage_tips": "Erweiterte Zyklen: 'steigend' oder 'stabil' = guter Zeitpunkt zum Starten, Preise werden nicht günstiger. 'fallend' = warten wenn dein Zeitplan es erlaubt. Passt sich dem Markt an — findet bestes relatives Timing in jedem Preisumfeld."
},
"price_trend_6h": {
"description": "Preistrend für die nächsten 6 Stunden",
"long_description": "Vergleicht aktuellen Intervallpreis mit Durchschnitt der nächsten 6 Stunden (24 Intervalle). Steigend wenn Zukunft >5% höher, fallend wenn >5% niedriger, sonst stabil.",
"usage_tips": "Relative Optimierung: Abendentscheidungen. 'fallend' = Preise verbessern sich deutlich wenn du wartest. Keine festen Schwellenwerte nötig - passt sich automatisch an Winter/Sommer-Preisniveaus an."
"long_description": "Vergleicht deinen aktuellen Preis mit dem Durchschnitt aller Intervalle der nächsten 6 Stunden (24 Intervalle) — das gesamte Zeitfenster ab jetzt, nicht nur der spätere Teil. Steigend/fallend ab ±3%, stark ab ±9% (konfigurierbar, volatilitätsadaptiv).",
"usage_tips": "Abendentscheidungen: 'steigend' = Strom jetzt nutzen, solange er relativ günstig ist. 'fallend' = Abend-/Nachtpreise werden besser, warte wenn möglich. Passt sich automatisch an Winter/Sommer-Preisniveaus an — keine festen Schwellenwerte nötig."
},
"price_trend_8h": {
"description": "Preistrend für die nächsten 8 Stunden",
"long_description": "Vergleicht aktuellen Intervallpreis mit Durchschnitt der nächsten 8 Stunden (32 Intervalle). Steigend wenn Zukunft >5% höher, fallend wenn >5% niedriger, sonst stabil.",
"usage_tips": "Relative Optimierung: Nachtplanung. 'fallend' bedeutet Warten auf Nacht lohnt sich (>5% günstiger). Funktioniert ganzjährig ohne manuelle Schwellenwert-Anpassungen. Starte wenn 'stabil' oder 'steigend'."
"long_description": "Vergleicht deinen aktuellen Preis mit dem Durchschnitt aller Intervalle der nächsten 8 Stunden (32 Intervalle) — das gesamte Zeitfenster ab jetzt, nicht nur der spätere Teil. Steigend/fallend ab ±3%, stark ab ±9% (konfigurierbar, volatilitätsadaptiv).",
"usage_tips": "Nachtplanung: 'steigend' = heute Nacht/morgen wird teurer, Strom jetzt nutzen. 'fallend' = Nachtpreise werden günstiger, Warten lohnt sich. 'stabil' = starte nach Bedarf. Funktioniert ganzjährig ohne manuelle Schwellenwert-Anpassungen."
},
"price_trend_12h": {
"description": "Preistrend für die nächsten 12 Stunden",
"long_description": "Vergleicht aktuellen Intervallpreis mit Durchschnitt der nächsten 12 Stunden (48 Intervalle). Steigend wenn Zukunft >5% höher, fallend wenn >5% niedriger, sonst stabil.",
"usage_tips": "Relative Optimierung: Langfristige strategische Entscheidungen. 'fallend' = deutlich bessere Preise kommen heute Nacht/morgen. Findet optimales Timing in jeder Marktsituation. Am besten kombiniert mit avg-Sensor Preisobergrenze."
"long_description": "Vergleicht deinen aktuellen Preis mit dem Durchschnitt aller Intervalle der nächsten 12 Stunden (48 Intervalle) — das gesamte Zeitfenster ab jetzt, nicht nur der spätere Teil. Steigend/fallend ab ±3%, stark ab ±9% (konfigurierbar, volatilitätsadaptiv).",
"usage_tips": "Strategische Entscheidungen: 'steigend' = du bist an einem Tiefpunkt, guter Zeitpunkt für stromintensive Aufgaben. 'fallend' = deutlich bessere Preise kommen, warte wenn möglich. Am besten kombiniert mit avg-Sensor für absolute Preisobergrenzen."
},
"current_price_trend": {
"description": "Aktuelle Preistrend-Richtung und wie lange sie anhält",
@ -274,9 +274,14 @@
},
"next_price_trend_change": {
"description": "Wann die nächste bedeutende Preistrend-Änderung eintreten wird",
"long_description": "Scannt die nächsten 24 Stunden (96 Intervalle), um zu finden, wann sich der Preistrend (steigend/fallend/stabil) vom aktuellen Momentum ändern wird. Bestimmt zuerst den aktuellen Trend mit gewichtetem 1h-Rückblick (erkennt laufende Trends), dann findet es die Umkehr. Verwendet volatilitätsadaptive Schwellwerte (3% Momentum-Erkennung, marktangepasster Zukunftsvergleich). Gibt den Zeitstempel zurück, wann die Änderung erwartet wird.",
"long_description": "Scannt die nächsten 24 Stunden (96 Intervalle), um zu finden, wann sich die Preistrend-Richtung ändern wird. Nur Richtungswechsel zählen: steigend/stark steigend bilden eine Gruppe, fallend/stark fallend eine andere, stabil ist eigenständig. Ein Wechsel von steigend zu stark steigend ist KEIN Trendwechsel. Verwendet volatilitätsadaptive Schwellwerte (Standard: ±3%/±9%) mit Hysterese (Standard: 3 aufeinanderfolgende Intervalle). Gibt den Zeitstempel zurück, wann die Änderung erwartet wird.",
"usage_tips": "Ereignisbasierte Automation: Aktionen WENN Trend wechselt auslösen, nicht IN X Stunden. Beispiel: 'E-Auto laden wenn nächste Trendänderung fallende Preise zeigt' oder 'Spülmaschine vor Preisanstieg starten'. Ergänzt Zeitfenster-Sensoren (price_trend_Xh), die beantworten 'WERDEN Preise in X Stunden höher sein?'"
},
"trend_change_in_minutes": {
"description": "Zeit bis zur nächsten Preistrend-Änderung",
"long_description": "Zeigt an, wie lange es bis zur nächsten bedeutenden Preistrend-Änderung dauert. Der Wert wird in Stunden angezeigt (z.B. 2,25 h) für Dashboards. Teilt die gleiche Analyse wie der Zeitstempel-Sensor 'Nächste Trendänderung', stellt sie aber als Countdown-Dauer dar. Aktualisiert sich jede Minute für präzise Countdowns. Zeigt 'Unbekannt' wenn keine Trendänderung in den nächsten 24 Stunden erwartet wird.",
"usage_tips": "Dashboard-Countdown: Zeige 'Trendänderung in 1,5 h' als Live-Countdown. Für Automationen: 'Wenn trend_change_in_minutes < 0,25 (15 Min), auf Preisrichtungswechsel vorbereiten'. Ergänzt 'Nächste Trendänderung' (Zeitstempel) — verwende den Zeitstempel für 'WANN' und diesen Sensor für 'WIE LANGE'."
},
"daily_rating": {
"description": "Wie sich die heutigen Preise mit historischen Daten vergleichen",
"long_description": "Zeigt, wie sich die heutigen Preise im Vergleich zu historischen Preisdaten als Prozentsatz verhält",

View file

@ -229,43 +229,43 @@
},
"price_trend_1h": {
"description": "Price trend for the next hour",
"long_description": "Compares current interval price with average of next 1 hour (4 intervals). Rising if future is >5% higher, falling if >5% lower, stable otherwise.",
"usage_tips": "Relative optimization: 'falling' = wait, prices dropping. 'rising' = act now or you'll pay more. 'stable' = price doesn't matter much now. Works independent of absolute price level."
"long_description": "Compares your current price with the average of all intervals in the next hour (4 intervals). All trend sensors (1h12h) share the same base: your current price — they differ only in window size. Larger windows include more future hours and smooth out short-term spikes. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"usage_tips": "Decision guide: 'rising' = ACT NOW, your current price is cheaper than the next hour. 'falling' = WAIT, cheaper prices coming. 'stable' = timing doesn't matter. Common misconception: 'rising' does NOT mean 'too late' — it means right now is a good price! Works regardless of absolute price level."
},
"price_trend_2h": {
"description": "Price trend for the next 2 hours",
"long_description": "Compares current interval price with average of next 2 hours (8 intervals). Rising if future is >5% higher, falling if >5% lower, stable otherwise.",
"usage_tips": "Relative optimization: Ideal for appliances. 'falling' means better prices coming in 2h - postpone if possible. Finds best timing within your available window, regardless of season."
"long_description": "Compares your current price with the average of all intervals in the next 2 hours (8 intervals) — the entire window from now, not just the later part. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"usage_tips": "Appliances: 'rising' = start now, you're at a good price. 'falling' = better prices coming in 2h, postpone if possible. 'stable' = doesn't matter, start when convenient. Don't wait for 'stable' — if it's 'rising', NOW is actually the best time."
},
"price_trend_3h": {
"description": "Price trend for the next 3 hours",
"long_description": "Compares current interval price with average of next 3 hours (12 intervals). Rising if future is >5% higher, falling if >5% lower, stable otherwise.",
"usage_tips": "Relative optimization: For Eco programs. 'falling' means prices dropping >5% - worth waiting. Works in any season. Combine with avg sensor for price cap: only when avg < your limit AND trend not 'falling'."
"long_description": "Compares your current price with the average of all intervals in the next 3 hours (12 intervals) — the entire window from now, not just the later part. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"usage_tips": "Eco programs: 'rising' = start the eco cycle now, prices go up from here. 'falling' = wait, cheaper window coming. Combine with avg sensor for price cap: start when trend is 'rising' or 'stable' AND avg < your limit. Works in any season."
},
"price_trend_4h": {
"description": "Price trend for the next 4 hours",
"long_description": "Compares current interval price with average of next 4 hours (16 intervals). Rising if future is >5% higher, falling if >5% lower, stable otherwise.",
"usage_tips": "Relative optimization: Heat pump/battery decisions. 'falling' means better charging window coming. Always finds relative best time - whether prices are 10 cents or 50 cents. Use avg sensor for absolute limit."
"long_description": "Compares your current price with the average of all intervals in the next 4 hours (16 intervals) — the entire window from now, not just the later part. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"usage_tips": "Heat pump/battery: 'rising' = charge now, you're at a relative low. 'falling' = wait for better charging window. 'stable' = charge when needed. Works regardless of absolute price — finds relative best time whether prices are 10 or 50 cents."
},
"price_trend_5h": {
"description": "Price trend for the next 5 hours",
"long_description": "Compares current interval price with average of next 5 hours (20 intervals). Rising if future is >5% higher, falling if >5% lower, stable otherwise.",
"usage_tips": "Relative optimization: Extended operations. Adapts to market - finds best relative timing in any price environment. 'stable/rising' = good time to start within your planning window."
"long_description": "Compares your current price with the average of all intervals in the next 5 hours (20 intervals) — the entire window from now, not just the later part. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"usage_tips": "Extended operations: 'rising' or 'stable' = good time to start, prices won't be cheaper. 'falling' = wait if your schedule allows. Adapts to market conditions — finds best relative timing in any price environment."
},
"price_trend_6h": {
"description": "Price trend for the next 6 hours",
"long_description": "Compares current interval price with average of next 6 hours (24 intervals). Rising if future is >5% higher, falling if >5% lower, stable otherwise.",
"usage_tips": "Relative optimization: Evening decisions. 'falling' = prices improve significantly if you wait. No fixed thresholds needed - automatically adjusts to winter/summer price levels."
"long_description": "Compares your current price with the average of all intervals in the next 6 hours (24 intervals) — the entire window from now, not just the later part. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"usage_tips": "Evening decisions: 'rising' = use electricity now while it's relatively cheap. 'falling' = evening/night prices will be better, wait if possible. Automatically adjusts to winter/summer price levels — no fixed thresholds needed."
},
"price_trend_8h": {
"description": "Price trend for the next 8 hours",
"long_description": "Compares current interval price with average of next 8 hours (32 intervals). Rising if future is >5% higher, falling if >5% lower, stable otherwise.",
"usage_tips": "Relative optimization: Night planning. 'falling' means waiting for night pays off (>5% cheaper). Works year-round without manual threshold adjustments. Start when 'stable' or 'rising'."
"long_description": "Compares your current price with the average of all intervals in the next 8 hours (32 intervals) — the entire window from now, not just the later part. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"usage_tips": "Night planning: 'rising' = tonight/tomorrow will be more expensive, use power now. 'falling' = night prices will be cheaper, worth waiting. 'stable' = start when convenient. Works year-round without manual threshold adjustments."
},
"price_trend_12h": {
"description": "Price trend for the next 12 hours",
"long_description": "Compares current interval price with average of next 12 hours (48 intervals). Rising if future is >5% higher, falling if >5% lower, stable otherwise.",
"usage_tips": "Relative optimization: Long-term strategic decisions. 'falling' = significantly better prices coming tonight/tomorrow. Finds optimal timing in any market condition. Best combined with avg sensor price cap."
"long_description": "Compares your current price with the average of all intervals in the next 12 hours (48 intervals) — the entire window from now, not just the later part. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"usage_tips": "Strategic decisions: 'rising' = you're at a low point, good time for high-consumption tasks. 'falling' = significantly better prices coming, wait if possible. Best combined with avg sensor price cap for absolute limits."
},
"current_price_trend": {
"description": "Current price trend direction and how long it will last",
@ -274,9 +274,14 @@
},
"next_price_trend_change": {
"description": "When the next significant price trend change will occur",
"long_description": "Scans the next 24 hours (96 intervals) to find when the price trend (rising/falling/stable) will change from the current momentum. First determines current trend using weighted 1h lookback (recognizes ongoing trends), then finds when that trend reverses. Uses volatility-adaptive thresholds (3% momentum detection, market-adjusted future comparison). Returns the timestamp when the change is expected.",
"long_description": "Scans the next 24 hours (96 intervals) to find when the price trend direction will change. Only direction changes count: rising/strongly_rising are one group, falling/strongly_falling another, stable is its own. A change from rising to strongly_rising is NOT a trend change. Uses volatility-adaptive thresholds (default: ±3%/±9%) with hysteresis (default: 3 consecutive intervals). Returns the timestamp when the change is expected.",
"usage_tips": "Event-based automation: Trigger actions WHEN trend changes, not IN X hours. Example: 'Charge EV when next trend change shows falling prices' or 'Run dishwasher before prices start rising'. More accurate than simple future comparison because it knows if you're already in a trend. Complements time-window sensors (price_trend_Xh) which answer 'WILL prices be higher in X hours?'"
},
"trend_change_in_minutes": {
"description": "Time until the next price trend change",
"long_description": "Shows how long until the next significant price trend change occurs. The state displays in hours (e.g., 2.25 h) for dashboards. Shares the same analysis as the Next Price Trend Change timestamp sensor but presents it as a countdown duration. Updates every minute for accurate countdowns. Returns unknown when no trend change is expected in the next 24 hours.",
"usage_tips": "Dashboard countdown: Show 'Trend changes in 1.5 h' as a live countdown. For automations: 'If trend_change_in_minutes state < 0.25 (15 min), prepare for price direction change'. Pairs with Next Price Trend Change (timestamp) — use the timestamp for 'WHEN' and this sensor for 'HOW LONG'."
},
"daily_rating": {
"description": "How today's prices compare to historical data",
"long_description": "Shows how today's prices compare to historical price data as a percentage",

View file

@ -229,43 +229,43 @@
},
"price_trend_1h": {
"description": "Pristrend for neste time",
"long_description": "Sammenligner nåværende intervallpris med gjennomsnitt av neste 1 time (4 intervaller). Stigende hvis fremtiden er >5% høyere, fallende hvis >5% lavere, ellers stabil.",
"usage_tips": "Relativ optimalisering: 'fallende' = vent, prisene faller. 'stigende' = handle nå eller du betaler mer. 'stabil' = prisen spiller ikke så stor rolle nå. Fungerer uavhengig av absolutt prisnivå."
"long_description": "Sammenligner din nåværende pris med gjennomsnittet av alle intervaller den neste timen (4 intervaller). Alle trendsensorer (1t12t) har samme utgangspunkt: din nåværende pris — de skiller seg bare i vindustørrelse. Større vinduer dekker flere fremtidige timer og jevner ut kortsiktige topper. Stigende/fallende ved ±3%, sterkt ved ±9% (konfigurerbart, volatilitetstilpasset).",
"usage_tips": "Beslutningshjelp: 'stigende' = HANDLE NÅ, din nåværende pris er gunstigere enn neste time. 'fallende' = VENT, billigere priser kommer. 'stabil' = timing spiller ingen rolle. Vanlig misforståelse: 'stigende' betyr IKKE 'for sent' — det betyr at nå er en god pris! Fungerer uavhengig av absolutt prisnivå."
},
"price_trend_2h": {
"description": "Pristrend for neste 2 timer",
"long_description": "Sammenligner nåværende intervallpris med gjennomsnitt av neste 2 timer (8 intervaller). Stigende hvis fremtiden er >5% høyere, fallende hvis >5% lavere, ellers stabil.",
"usage_tips": "Relativ optimalisering: Ideelt for apparater. 'fallende' betyr bedre priser kommer om 2t - utsett hvis mulig. Finner beste timing innenfor ditt tilgjengelige vindu, uavhengig av sesong."
"long_description": "Sammenligner din nåværende pris med gjennomsnittet av alle intervaller de neste 2 timene (8 intervaller) — hele vinduet fra nå, ikke bare den senere delen. Stigende/fallende ved ±3%, sterkt ved ±9% (konfigurerbart, volatilitetstilpasset).",
"usage_tips": "Apparater: 'stigende' = start nå, du har en god pris. 'fallende' = bedre priser kommer om 2t, utsett hvis mulig. 'stabil' = spiller ingen rolle, start når det passer. Ikke vent på 'stabil' — ved 'stigende' er NÅ det beste tidspunktet."
},
"price_trend_3h": {
"description": "Pristrend for neste 3 timer",
"long_description": "Sammenligner nåværende intervallpris med gjennomsnitt av neste 3 timer (12 intervaller). Stigende hvis fremtiden er >5% høyere, fallende hvis >5% lavere, ellers stabil.",
"usage_tips": "Relativ optimalisering: For Eco-programmer. 'fallende' betyr priser faller >5% - verdt å vente. Fungerer i enhver sesong. Kombiner med avg-sensor for pristak: kun når avg < din grense OG trend ikke 'fallende'."
"long_description": "Sammenligner din nåværende pris med gjennomsnittet av alle intervaller de neste 3 timene (12 intervaller) — hele vinduet fra nå, ikke bare den senere delen. Stigende/fallende ved ±3%, sterkt ved ±9% (konfigurerbart, volatilitetstilpasset).",
"usage_tips": "Eco-programmer: 'stigende' = start eco-syklusen nå, prisene stiger herfra. 'fallende' = vent, billigere vindu kommer. Kombiner med avg-sensor: start når trend er 'stigende' eller 'stabil' OG avg < din grense. Fungerer i enhver sesong."
},
"price_trend_4h": {
"description": "Pristrend for neste 4 timer",
"long_description": "Sammenligner nåværende intervallpris med gjennomsnitt av neste 4 timer (16 intervaller). Stigende hvis fremtiden er >5% høyere, fallende hvis >5% lavere, ellers stabil.",
"usage_tips": "Relativ optimalisering: Varmepumpe/batteribeslutninger. 'fallende' betyr bedre ladevindu kommer. Finner alltid relativ beste tid - enten prisene er 10 cent eller 50 cent. Bruk avg-sensor for absolutt grense."
"long_description": "Sammenligner din nåværende pris med gjennomsnittet av alle intervaller de neste 4 timene (16 intervaller) — hele vinduet fra nå, ikke bare den senere delen. Stigende/fallende ved ±3%, sterkt ved ±9% (konfigurerbart, volatilitetstilpasset).",
"usage_tips": "Varmepumpe/batteri: 'stigende' = lad nå, du er på et relativt lavpunkt. 'fallende' = vent på bedre ladevindu. 'stabil' = lad etter behov. Fungerer uavhengig av prisnivå — finner relativ beste tid enten prisene er 10 eller 50 øre."
},
"price_trend_5h": {
"description": "Pristrend for neste 5 timer",
"long_description": "Sammenligner nåværende intervallpris med gjennomsnitt av neste 5 timer (20 intervaller). Stigende hvis fremtiden er >5% høyere, fallende hvis >5% lavere, ellers stabil.",
"usage_tips": "Relativ optimalisering: Utvidede operasjoner. Tilpasser seg markedet - finner beste relative timing i ethvert prismiljø. 'stabil/stigende' = godt tidspunkt å starte innenfor ditt planleggingsvindu."
"long_description": "Sammenligner din nåværende pris med gjennomsnittet av alle intervaller de neste 5 timene (20 intervaller) — hele vinduet fra nå, ikke bare den senere delen. Stigende/fallende ved ±3%, sterkt ved ±9% (konfigurerbart, volatilitetstilpasset).",
"usage_tips": "Utvidede sykluser: 'stigende' eller 'stabil' = godt tidspunkt å starte, prisene blir ikke billigere. 'fallende' = vent hvis planen din tillater det. Tilpasser seg markedet — finner beste relative timing i ethvert prismiljø."
},
"price_trend_6h": {
"description": "Pristrend for neste 6 timer",
"long_description": "Sammenligner nåværende intervallpris med gjennomsnitt av neste 6 timer (24 intervaller). Stigende hvis fremtiden er >5% høyere, fallende hvis >5% lavere, ellers stabil.",
"usage_tips": "Relativ optimalisering: Kveldsbeslutninger. 'fallende' = prisene forbedres betydelig hvis du venter. Ingen faste terskler nødvendig - justerer automatisk til vinter/sommer prisnivåer."
"long_description": "Sammenligner din nåværende pris med gjennomsnittet av alle intervaller de neste 6 timene (24 intervaller) — hele vinduet fra nå, ikke bare den senere delen. Stigende/fallende ved ±3%, sterkt ved ±9% (konfigurerbart, volatilitetstilpasset).",
"usage_tips": "Kveldsbeslutninger: 'stigende' = bruk strøm nå mens den er relativt billig. 'fallende' = kvelds-/nattprisene blir bedre, vent hvis mulig. Justerer automatisk til vinter/sommer prisnivåer — ingen faste terskler nødvendig."
},
"price_trend_8h": {
"description": "Pristrend for neste 8 timer",
"long_description": "Sammenligner nåværende intervallpris med gjennomsnitt av neste 8 timer (32 intervaller). Stigende hvis fremtiden er >5% høyere, fallende hvis >5% lavere, ellers stabil.",
"usage_tips": "Relativ optimalisering: Nattplanlegging. 'fallende' betyr at å vente til natten lønner seg (>5% billigere). Fungerer hele året uten manuelle terskeljusteringer. Start når 'stabil' eller 'stigende'."
"long_description": "Sammenligner din nåværende pris med gjennomsnittet av alle intervaller de neste 8 timene (32 intervaller) — hele vinduet fra nå, ikke bare den senere delen. Stigende/fallende ved ±3%, sterkt ved ±9% (konfigurerbart, volatilitetstilpasset).",
"usage_tips": "Nattplanlegging: 'stigende' = i natt/i morgen blir dyrere, bruk strøm nå. 'fallende' = nattprisene blir billigere, verdt å vente. 'stabil' = start etter behov. Fungerer hele året uten manuelle terskeljusteringer."
},
"price_trend_12h": {
"description": "Pristrend for de neste 12 timene",
"long_description": "Sammenligner nåværende intervallpris med gjennomsnittet av de neste 12 timene (48 intervaller). Økende hvis framtidig pris er >5% høyere, synkende hvis >5% lavere, ellers stabil.",
"usage_tips": "Relativ optimalisering: Langsiktige strategiske beslutninger. 'synkende' = betydelig bedre priser kommer i natt/i morgen. Finner optimal timing i enhver markedssituasjon. Best kombinert med prisgrense fra avg-sensor."
"long_description": "Sammenligner din nåværende pris med gjennomsnittet av alle intervaller de neste 12 timene (48 intervaller) — hele vinduet fra nå, ikke bare den senere delen. Stigende/fallende ved ±3%, sterkt ved ±9% (konfigurerbart, volatilitetstilpasset).",
"usage_tips": "Strategiske beslutninger: 'stigende' = du er på et lavpunkt, godt tidspunkt for strømkrevende oppgaver. 'fallende' = betydelig bedre priser kommer, vent hvis mulig. Best kombinert med avg-sensor for absolutte prisgrenser."
},
"current_price_trend": {
"description": "Nåværende pristrend-retning og hvor lenge den varer",
@ -274,9 +274,14 @@
},
"next_price_trend_change": {
"description": "Når neste betydelige pristrendendring vil skje",
"long_description": "Skanner de neste 24 timene (96 intervaller) for å finne når pristrenden (økende/synkende/stabil) vil endre seg fra nåværende momentum. Bestemmer først nåværende trend med vektet 1t tilbakeblikk (gjenkjenner pågående trender), deretter finner den reverseringen. Bruker volatilitetsadaptive terskelverdier (3 % momentum-deteksjon, markedsjustert fremtidssammenligning). Returnerer tidsstempelet når endringen forventes.",
"long_description": "Skanner de neste 24 timene (96 intervaller) for å finne når pristrend-retningen vil endre seg. Kun retningsendringer teller: stigende/sterkt stigende er én gruppe, fallende/sterkt fallende en annen, stabil er egen. En endring fra stigende til sterkt stigende er IKKE en trendendring. Bruker volatilitetstilpassede terskelverdier (standard: ±3%/±9%) med hysterese (standard: 3 påfølgende intervaller). Returnerer tidsstempelet når endringen forventes.",
"usage_tips": "Hendelsesbasert automatisering: Utløs handlinger NÅR trenden endres, ikke OM X timer. Eksempel: 'Lad EV når neste trendendring viser synkende priser' eller 'Start oppvaskmaskin før prisene stiger'. Kompletterer tidsvindu-sensorer (price_trend_Xh) som svarer på 'VIL prisene være høyere om X timer?'"
},
"trend_change_in_minutes": {
"description": "Tid til neste pristrendendring",
"long_description": "Viser hvor lenge det er til neste betydelige pristrendendring inntreffer. Verdien vises i timer (f.eks. 2,25 t) for dashboards. Deler samme analyse som tidsstempel-sensoren 'Neste trendendring', men presenterer den som en nedtellingsvarighet. Oppdateres hvert minutt for nøyaktige nedtellinger. Viser 'Ukjent' når ingen trendendring forventes i løpet av de neste 24 timene.",
"usage_tips": "Dashboard-nedtelling: Vis 'Trendendring om 1,5 t' som live nedtelling. For automatiseringer: 'Hvis trend_change_in_minutes < 0,25 (15 min), forbered på prisretningsendring'. Kompletterer 'Neste trendendring' (tidsstempel) — bruk tidsstempelet for 'NÅR' og denne sensoren for 'HVOR LENGE'."
},
"daily_rating": {
"description": "Hvordan dagens priser sammenlignes med historiske data",
"long_description": "Viser hvordan dagens priser sammenlignes med historiske prisdata som en prosentandel",

View file

@ -229,43 +229,43 @@
},
"price_trend_1h": {
"description": "Prijstrend voor het volgende uur",
"long_description": "Vergelijkt huidige intervalprijs met gemiddelde van volgend 1 uur (4 intervallen). Stijgend als toekomst >5% hoger is, dalend als >5% lager, anders stabiel.",
"usage_tips": "Relatieve optimalisatie: 'dalend' = wacht, prijzen dalen. 'stijgend' = handel nu of je betaalt meer. 'stabiel' = prijs maakt nu niet veel uit. Werkt onafhankelijk van absoluut prijsniveau."
"long_description": "Vergelijkt je huidige prijs met het gemiddelde van alle intervallen in het volgende uur (4 intervallen). Alle trendsensoren (1u12u) delen hetzelfde uitgangspunt: je huidige prijs — ze verschillen alleen in venstergrootte. Grotere vensters omvatten meer toekomstige uren en vlakken kortstondige pieken af. Stijgend/dalend bij ±3%, sterk bij ±9% (configureerbaar, volatiliteitsadaptief).",
"usage_tips": "Beslissingshulp: 'stijgend' = HANDEL NU, je huidige prijs is goedkoper dan het volgende uur. 'dalend' = WACHT, goedkopere prijzen komen. 'stabiel' = timing maakt niet uit. Veelvoorkomend misverstand: 'stijgend' betekent NIET 'te laat' — het betekent dat nu een goede prijs is! Werkt onafhankelijk van absoluut prijsniveau."
},
"price_trend_2h": {
"description": "Prijstrend voor de volgende 2 uur",
"long_description": "Vergelijkt huidige intervalprijs met gemiddelde van volgende 2 uur (8 intervallen). Stijgend als toekomst >5% hoger is, dalend als >5% lager, anders stabiel.",
"usage_tips": "Relatieve optimalisatie: Ideaal voor apparaten. 'dalend' betekent betere prijzen komen over 2u - stel uit indien mogelijk. Vindt beste timing binnen je beschikbare venster, ongeacht seizoen."
"long_description": "Vergelijkt je huidige prijs met het gemiddelde van alle intervallen in de volgende 2 uur (8 intervallen) — het hele venster vanaf nu, niet alleen het latere deel. Stijgend/dalend bij ±3%, sterk bij ±9% (configureerbaar, volatiliteitsadaptief).",
"usage_tips": "Apparaten: 'stijgend' = start nu, je hebt een goede prijs. 'dalend' = betere prijzen komen over 2u, stel uit indien mogelijk. 'stabiel' = maakt niet uit, start wanneer het uitkomt. Wacht niet op 'stabiel' — bij 'stijgend' is NU het beste moment."
},
"price_trend_3h": {
"description": "Prijstrend voor de volgende 3 uur",
"long_description": "Vergelijkt huidige intervalprijs met gemiddelde van volgende 3 uur (12 intervallen). Stijgend als toekomst >5% hoger is, dalend als >5% lager, anders stabiel.",
"usage_tips": "Relatieve optimalisatie: Voor Eco-programma's. 'dalend' betekent prijzen dalen >5% - het wachten waard. Werkt in elk seizoen. Combineer met avg-sensor voor prijslimiet: alleen wanneer avg < je limiet EN trend niet 'dalend'."
"long_description": "Vergelijkt je huidige prijs met het gemiddelde van alle intervallen in de volgende 3 uur (12 intervallen) — het hele venster vanaf nu, niet alleen het latere deel. Stijgend/dalend bij ±3%, sterk bij ±9% (configureerbaar, volatiliteitsadaptief).",
"usage_tips": "Eco-programma's: 'stijgend' = start de eco-cyclus nu, prijzen stijgen vanaf hier. 'dalend' = wacht, goedkoper venster komt. Combineer met avg-sensor: start wanneer trend 'stijgend' of 'stabiel' EN avg < je limiet. Werkt in elk seizoen."
},
"price_trend_4h": {
"description": "Prijstrend voor de volgende 4 uur",
"long_description": "Vergelijkt huidige intervalprijs met gemiddelde van volgende 4 uur (16 intervallen). Stijgend als toekomst >5% hoger is, dalend als >5% lager, anders stabiel.",
"usage_tips": "Relatieve optimalisatie: Warmtepomp/batterij beslissingen. 'dalend' betekent beter laadvenster komt. Vindt altijd relatief beste tijd - of prijzen nu 10 cent of 50 cent zijn. Gebruik avg-sensor voor absolute limiet."
"long_description": "Vergelijkt je huidige prijs met het gemiddelde van alle intervallen in de volgende 4 uur (16 intervallen) — het hele venster vanaf nu, niet alleen het latere deel. Stijgend/dalend bij ±3%, sterk bij ±9% (configureerbaar, volatiliteitsadaptief).",
"usage_tips": "Warmtepomp/batterij: 'stijgend' = laad nu, je zit op een relatief dieptepunt. 'dalend' = wacht op beter laadvenster. 'stabiel' = laad wanneer nodig. Werkt ongeacht prijsniveau — vindt relatief beste tijd of prijzen nu 10 of 50 cent zijn."
},
"price_trend_5h": {
"description": "Prijstrend voor de volgende 5 uur",
"long_description": "Vergelijkt huidige intervalprijs met gemiddelde van volgende 5 uur (20 intervallen). Stijgend als toekomst >5% hoger is, dalend als >5% lager, anders stabiel.",
"usage_tips": "Relatieve optimalisatie: Uitgebreide operaties. Past zich aan de markt aan - vindt beste relatieve timing in elke prijsomgeving. 'stabiel/stijgend' = goed moment om te starten binnen je planningsvenster."
"long_description": "Vergelijkt je huidige prijs met het gemiddelde van alle intervallen in de volgende 5 uur (20 intervallen) — het hele venster vanaf nu, niet alleen het latere deel. Stijgend/dalend bij ±3%, sterk bij ±9% (configureerbaar, volatiliteitsadaptief).",
"usage_tips": "Uitgebreide cycli: 'stijgend' of 'stabiel' = goed moment om te starten, prijzen worden niet goedkoper. 'dalend' = wacht als je planning het toelaat. Past zich aan de markt aan — vindt beste relatieve timing in elke prijsomgeving."
},
"price_trend_6h": {
"description": "Prijstrend voor de volgende 6 uur",
"long_description": "Vergelijkt huidige intervalprijs met gemiddelde van volgende 6 uur (24 intervallen). Stijgend als toekomst >5% hoger is, dalend als >5% lager, anders stabiel.",
"usage_tips": "Relatieve optimalisatie: Avandbeslissingen. 'dalend' = prijzen verbeteren aanzienlijk als je wacht. Geen vaste drempels nodig - past automatisch aan winter/zomer prijsniveaus."
"long_description": "Vergelijkt je huidige prijs met het gemiddelde van alle intervallen in de volgende 6 uur (24 intervallen) — het hele venster vanaf nu, niet alleen het latere deel. Stijgend/dalend bij ±3%, sterk bij ±9% (configureerbaar, volatiliteitsadaptief).",
"usage_tips": "Avandbeslissingen: 'stijgend' = gebruik stroom nu terwijl het relatief goedkoop is. 'dalend' = avond-/nachtprijzen worden beter, wacht indien mogelijk. Past automatisch aan winter/zomer prijsniveaus aan — geen vaste drempels nodig."
},
"price_trend_8h": {
"description": "Prijstrend voor de volgende 8 uur",
"long_description": "Vergelijkt huidige intervalprijs met gemiddelde van volgende 8 uur (32 intervallen). Stijgend als toekomst >5% hoger is, dalend als >5% lager, anders stabiel.",
"usage_tips": "Relatieve optimalisatie: Nachtplanning. 'dalend' betekent wachten tot de nacht loont (>5% goedkoper). Werkt het hele jaar door zonder handmatige drempelaanpassingen. Start wanneer 'stabiel' of 'stijgend'."
"long_description": "Vergelijkt je huidige prijs met het gemiddelde van alle intervallen in de volgende 8 uur (32 intervallen) — het hele venster vanaf nu, niet alleen het latere deel. Stijgend/dalend bij ±3%, sterk bij ±9% (configureerbaar, volatiliteitsadaptief).",
"usage_tips": "Nachtplanning: 'stijgend' = vanavond/morgen wordt duurder, gebruik stroom nu. 'dalend' = nachtprijzen worden goedkoper, wachten loont. 'stabiel' = start wanneer het uitkomt. Werkt het hele jaar door zonder handmatige drempelaanpassingen."
},
"price_trend_12h": {
"description": "Prijstrend voor de komende 12 uur",
"long_description": "Vergelijkt huidige intervalprijs met gemiddelde van de komende 12 uur (48 intervallen). Stijgend als toekomst >5% hoger is, dalend als >5% lager, anders stabiel.",
"usage_tips": "Relatieve optimalisatie: Lange termijn strategische beslissingen. 'dalend' = aanzienlijk betere prijzen komen vanavond/morgen. Vindt optimale timing in elke marktsituatie. Het beste gecombineerd met prijslimiet van avg-sensor."
"long_description": "Vergelijkt je huidige prijs met het gemiddelde van alle intervallen in de komende 12 uur (48 intervallen) — het hele venster vanaf nu, niet alleen het latere deel. Stijgend/dalend bij ±3%, sterk bij ±9% (configureerbaar, volatiliteitsadaptief).",
"usage_tips": "Strategische beslissingen: 'stijgend' = je zit op een dieptepunt, goed moment voor stroomintensieve taken. 'dalend' = aanzienlijk betere prijzen komen, wacht indien mogelijk. Het beste gecombineerd met avg-sensor voor absolute prijslimieten."
},
"current_price_trend": {
"description": "Huidige prijstrend-richting en hoe lang deze aanhoudt",
@ -274,9 +274,14 @@
},
"next_price_trend_change": {
"description": "Wanneer de volgende significante prijstrendwijziging zal plaatsvinden",
"long_description": "Scant de komende 24 uur (96 intervallen) om te vinden wanneer de prijstrend (stijgend/dalend/stabiel) zal veranderen ten opzichte van het huidige momentum. Bepaalt eerst de huidige trend met gewogen 1u terugblik (herkent lopende trends), vindt dan de omkering. Gebruikt volatiliteit-adaptieve drempelwaarden (3% momentum-detectie, marktaangepaste toekomstvergelijking). Retourneert het tijdstempel wanneer de wijziging wordt verwacht.",
"long_description": "Scant de komende 24 uur (96 intervallen) om te vinden wanneer de prijstrend-richting zal veranderen. Alleen richtingswijzigingen tellen: stijgend/sterk stijgend vormen één groep, dalend/sterk dalend een andere, stabiel is apart. Een verandering van stijgend naar sterk stijgend is GEEN trendwijziging. Gebruikt volatiliteit-adaptieve drempelwaarden (standaard: ±3%/±9%) met hysterese (standaard: 3 opeenvolgende intervallen). Retourneert het tijdstempel wanneer de wijziging wordt verwacht.",
"usage_tips": "Gebeurtenisgestuurde automatisering: Trigger acties WANNEER trend wijzigt, niet OVER X uur. Voorbeeld: 'Laad EV wanneer volgende trendwijziging dalende prijzen toont' of 'Start vaatwasser voordat prijzen stijgen'. Vult tijdvenster-sensors aan (price_trend_Xh) die beantwoorden 'ZULLEN prijzen over X uur hoger zijn?'"
},
"trend_change_in_minutes": {
"description": "Tijd tot de volgende prijstrendwijziging",
"long_description": "Toont hoe lang het duurt tot de volgende significante prijstrendwijziging plaatsvindt. De waarde wordt weergegeven in uren (bijv. 2,25 u) voor dashboards. Deelt dezelfde analyse als de tijdstempel-sensor 'Volgende Prijstrend Wijziging', maar presenteert het als een aftelduur. Wordt elke minuut bijgewerkt voor nauwkeurige aftellingen. Toont 'Onbekend' wanneer geen trendwijziging wordt verwacht in de komende 24 uur.",
"usage_tips": "Dashboard-aftelling: Toon 'Trendwijziging over 1,5 u' als live aftelling. Voor automatiseringen: 'Als trend_change_in_minutes < 0,25 (15 min), bereid je voor op prijsrichtingswijziging'. Vult 'Volgende Prijstrend Wijziging' (tijdstempel) aan — gebruik het tijdstempel voor 'WANNEER' en deze sensor voor 'HOE LANG'."
},
"daily_rating": {
"description": "Hoe de prijzen van vandaag zich verhouden tot historische gegevens",
"long_description": "Toont hoe de prijzen van vandaag zich verhouden tot historische prijsgegevens als percentage",

View file

@ -229,43 +229,43 @@
},
"price_trend_1h": {
"description": "Pristrend för nästa timme",
"long_description": "Jämför nuvarande intervallpris med genomsnitt av nästa 1 timme (4 intervaller). Stigande om framtid är >5% högre, fallande om >5% lägre, annars stabil.",
"usage_tips": "Relativ optimering: 'fallande' = vänta, priser sjunker. 'stigande' = agera nu eller du betalar mer. 'stabil' = pris spelar ingen större roll nu. Fungerar oberoende av absolut prisnivå."
"long_description": "Jämför ditt nuvarande pris med genomsnittet av alla intervaller under nästa timme (4 intervaller). Alla trendsensorer (1t12t) delar samma utgångspunkt: ditt nuvarande pris — de skiljer sig bara i fönsterstorlek. Större fönster täcker fler framtida timmar och jämnar ut kortsiktiga toppar. Stigande/fallande vid ±3%, kraftigt vid ±9% (konfigurerbart, volatilitetsadaptivt).",
"usage_tips": "Beslutsstöd: 'stigande' = AGERA NU, ditt nuvarande pris är billigare än nästa timme. 'fallande' = VÄNTA, billigare priser kommer. 'stabil' = timing spelar ingen roll. Vanligt missförstånd: 'stigande' betyder INTE 'för sent' — det betyder att nu är ett bra pris! Fungerar oberoende av absolut prisnivå."
},
"price_trend_2h": {
"description": "Pristrend för nästa 2 timmar",
"long_description": "Jämför nuvarande intervallpris med genomsnitt av nästa 2 timmar (8 intervaller). Stigande om framtid är >5% högre, fallande om >5% lägre, annars stabil.",
"usage_tips": "Relativ optimering: Idealisk för apparater. 'fallande' betyder bättre priser kommer om 2t - skjut upp om möjligt. Hittar bästa timing inom ditt tillgängliga fönster, oavsett säsong."
"long_description": "Jämför ditt nuvarande pris med genomsnittet av alla intervaller under de nästa 2 timmarna (8 intervaller) — hela fönstret från nu, inte bara den senare delen. Stigande/fallande vid ±3%, kraftigt vid ±9% (konfigurerbart, volatilitetsadaptivt).",
"usage_tips": "Apparater: 'stigande' = starta nu, du har ett bra pris. 'fallande' = bättre priser kommer om 2t, skjut upp om möjligt. 'stabil' = spelar ingen roll, starta när det passar. Vänta inte på 'stabil' — vid 'stigande' är NU den bästa tiden."
},
"price_trend_3h": {
"description": "Pristrend för nästa 3 timmar",
"long_description": "Jämför nuvarande intervallpris med genomsnitt av nästa 3 timmar (12 intervaller). Stigande om framtid är >5% högre, fallande om >5% lägre, annars stabil.",
"usage_tips": "Relativ optimering: För Eco-program. 'fallande' betyder priser sjunker >5% - värt att vänta. Fungerar under alla säsonger. Kombinera med avg-sensor för prisgräns: endast när avg < din gräns OCH trend inte 'fallande'."
"long_description": "Jämför ditt nuvarande pris med genomsnittet av alla intervaller under de nästa 3 timmarna (12 intervaller) — hela fönstret från nu, inte bara den senare delen. Stigande/fallande vid ±3%, kraftigt vid ±9% (konfigurerbart, volatilitetsadaptivt).",
"usage_tips": "Eco-program: 'stigande' = starta eco-cykeln nu, priserna stiger härifrån. 'fallande' = vänta, billigare fönster kommer. Kombinera med avg-sensor: starta när trend är 'stigande' eller 'stabil' OCH avg < din gräns. Fungerar under alla säsonger."
},
"price_trend_4h": {
"description": "Pristrend för nästa 4 timmar",
"long_description": "Jämför nuvarande intervallpris med genomsnitt av nästa 4 timmar (16 intervaller). Stigande om framtid är >5% högre, fallande om >5% lägre, annars stabil.",
"usage_tips": "Relativ optimering: Värmepump/batteribeslut. 'fallande' betyder bättre laddningsfönster kommer. Hittar alltid relativt bästa tid - oavsett om priserna är 10 öre eller 50 öre. Använd avg-sensor för absolut gräns."
"long_description": "Jämför ditt nuvarande pris med genomsnittet av alla intervaller under de nästa 4 timmarna (16 intervaller) — hela fönstret från nu, inte bara den senare delen. Stigande/fallande vid ±3%, kraftigt vid ±9% (konfigurerbart, volatilitetsadaptivt).",
"usage_tips": "Värmepump/batteri: 'stigande' = ladda nu, du är på en relativ lågpunkt. 'fallande' = vänta på bättre laddningsfönster. 'stabil' = ladda efter behov. Fungerar oavsett prisnivå — hittar relativ bästa tid oavsett om priserna är 10 eller 50 öre."
},
"price_trend_5h": {
"description": "Pristrend för nästa 5 timmar",
"long_description": "Jämför nuvarande intervallpris med genomsnitt av nästa 5 timmar (20 intervaller). Stigande om framtid är >5% högre, fallande om >5% lägre, annars stabil.",
"usage_tips": "Relativ optimering: Utökade operationer. Anpassar sig till marknaden - hittar bästa relativa timing i vilken prismiljö som helst. 'stabil/stigande' = bra tid att starta inom ditt planeringsfönster."
"long_description": "Jämför ditt nuvarande pris med genomsnittet av alla intervaller under de nästa 5 timmarna (20 intervaller) — hela fönstret från nu, inte bara den senare delen. Stigande/fallande vid ±3%, kraftigt vid ±9% (konfigurerbart, volatilitetsadaptivt).",
"usage_tips": "Utökade cykler: 'stigande' eller 'stabil' = bra tid att starta, priserna blir inte billigare. 'fallande' = vänta om din planering tillåter det. Anpassar sig till marknaden — hittar bästa relativa timing i vilken prismiljö som helst."
},
"price_trend_6h": {
"description": "Pristrend för nästa 6 timmar",
"long_description": "Jämför nuvarande intervallpris med genomsnitt av nästa 6 timmar (24 intervaller). Stigande om framtid är >5% högre, fallande om >5% lägre, annars stabil.",
"usage_tips": "Relativ optimering: Kvällsbeslut. 'fallande' = priser förbättras avsevärt om du väntar. Inga fasta trösklar behövs - justerar automatiskt till vinter/sommar prisnivåer."
"long_description": "Jämför ditt nuvarande pris med genomsnittet av alla intervaller under de nästa 6 timmarna (24 intervaller) — hela fönstret från nu, inte bara den senare delen. Stigande/fallande vid ±3%, kraftigt vid ±9% (konfigurerbart, volatilitetsadaptivt).",
"usage_tips": "Kvällsbeslut: 'stigande' = använd el nu medan den är relativt billig. 'fallande' = kvälls-/nattpriserna blir bättre, vänta om möjligt. Justerar automatiskt till vinter/sommar prisnivåer — inga fasta trösklar behövs."
},
"price_trend_8h": {
"description": "Pristrend för nästa 8 timmar",
"long_description": "Jämför nuvarande intervallpris med genomsnitt av nästa 8 timmar (32 intervaller). Stigande om framtid är >5% högre, fallande om >5% lägre, annars stabil.",
"usage_tips": "Relativ optimering: Nattplanering. 'fallande' betyder att vänta till natten lönar sig (>5% billigare). Fungerar året runt utan manuella tröskel justeringar. Starta när 'stabil' eller 'stigande'."
"long_description": "Jämför ditt nuvarande pris med genomsnittet av alla intervaller under de nästa 8 timmarna (32 intervaller) — hela fönstret från nu, inte bara den senare delen. Stigande/fallande vid ±3%, kraftigt vid ±9% (konfigurerbart, volatilitetsadaptivt).",
"usage_tips": "Nattplanering: 'stigande' = ikväll/imorgon blir dyrare, använd el nu. 'fallande' = nattpriserna blir billigare, värt att vänta. 'stabil' = starta efter behov. Fungerar året runt utan manuella tröskeljusteringar."
},
"price_trend_12h": {
"description": "Pristrend för nästa 12 timmar",
"long_description": "Jämför nuvarande intervallpris med genomsnitt av nästa 12 timmar (48 intervaller). Stigande om framtid är >5% högre, fallande om >5% lägre, annars stabil.",
"usage_tips": "Relativ optimering: Långsiktiga strategiska beslut. 'fallande' = avsevärt bättre priser kommer ikväll/imorgon. Hittar optimal timing i vilket marknadsläge som helst. Bäst kombinerad med avg-sensor prisgräns."
"long_description": "Jämför ditt nuvarande pris med genomsnittet av alla intervaller under de nästa 12 timmarna (48 intervaller) — hela fönstret från nu, inte bara den senare delen. Stigande/fallande vid ±3%, kraftigt vid ±9% (konfigurerbart, volatilitetsadaptivt).",
"usage_tips": "Strategiska beslut: 'stigande' = du är på en lågpunkt, bra tid för strömkrävande uppgifter. 'fallande' = avsevärt bättre priser kommer, vänta om möjligt. Bäst kombinerad med avg-sensor för absoluta prisgränser."
},
"current_price_trend": {
"description": "Nuvarande pristrend-riktning och hur länge den varar",
@ -274,9 +274,14 @@
},
"next_price_trend_change": {
"description": "När nästa betydande pristrendändring kommer att inträffa",
"long_description": "Skannar de nästa 24 timmarna (96 intervaller) för att hitta när pristrenden (stigande/fallande/stabil) kommer att ändras från nuvarande momentum. Bestämmer först nuvarande trend med viktad 1h tillbakablick (känner igen pågående trender), hittar sedan reverseringen. Använder volatilitetsadaptiva tröskelvärden (3 % momentum-detektering, marknadsanpassad framtidsjämförelse). Returnerar tidsstämpeln när ändringen förväntas.",
"long_description": "Skannar de nästa 24 timmarna (96 intervaller) för att hitta när pristrend-riktningen kommer att ändras. Bara riktningsändringar räknas: stigande/kraftigt stigande är en grupp, fallande/kraftigt fallande en annan, stabil är egen. En ändring från stigande till kraftigt stigande är INTE en trendändring. Använder volatilitetsadaptiva tröskelvärden (standard: ±3%/±9%) med hysteres (standard: 3 på varandra följande intervaller). Returnerar tidstämpeln när ändringen förväntas.",
"usage_tips": "Händelsestyrd automatisering: Utlös åtgärder NÄR trenden ändras, inte OM X timmar. Exempel: 'Ladda EV när nästa trendändring visar fallande priser' eller 'Starta diskmaskin innan priserna stiger'. Kompletterar tidsfönster-sensorer (price_trend_Xh) som svarar på 'KOMMER priserna att vara högre om X timmar?'"
},
"trend_change_in_minutes": {
"description": "Tid till nästa pristrendändring",
"long_description": "Visar hur lång tid det är kvar till nästa betydande pristrendändring inträffar. Värdet visas i timmar (t.ex. 2,25 h) för dashboards. Delar samma analys som tidstämpel-sensorn 'Nästa pristrendändring' men presenterar det som en nedtellningsvaraktighet. Uppdateras varje minut för noggranna nedtellningar. Visar 'Okänd' när ingen trendändring förväntas inom de närmaste 24 timmarna.",
"usage_tips": "Dashboard-nedtellning: Visa 'Trendändring om 1,5 h' som live nedtellning. För automatiseringar: 'Om trend_change_in_minutes < 0,25 (15 min), förbered för prisriktningsändring'. Kompletterar 'Nästa pristrendändring' (tidstämpel) — använd tidstämpeln för 'NÄR' och denna sensor för 'HUR LÄNGE'."
},
"daily_rating": {
"description": "Hur dagens priser jämförs med historiska data",
"long_description": "Visar hur dagens priser jämförs med historiska prisdata som en procentsats",

View file

@ -11,5 +11,5 @@
"requirements": [
"aiofiles>=23.2.1"
],
"version": "0.28.0"
"version": "0.29.0"
}

View file

@ -37,3 +37,6 @@ def _add_cached_trend_attributes(attributes: dict, key: str, cached_data: dict)
# Add cached attributes (timestamp already set by platform)
# State contains the timestamp of the trend change itself
attributes.update(cached_data["trend_change_attributes"])
elif key == "trend_change_in_minutes" and cached_data.get("trend_change_attributes"):
# Duration sensor shares same cached attributes as the timestamp sensor
attributes.update(cached_data["trend_change_attributes"])

View file

@ -3,9 +3,9 @@ Trend calculator for price trend analysis sensors.
This module handles all trend-related calculations:
- Simple price trends (1h-12h future comparison)
- Current trend with momentum analysis
- Next trend change prediction
- Trend duration tracking
- Current trend (pure future-based 3h outlook with volatility adjustment)
- Next trend change prediction (with configurable N-interval hysteresis, default 3)
- Trend duration tracking (lightweight price direction scan with noise tolerance)
Caching strategy:
- Simple trends: Cached per sensor update to ensure consistency between state and attributes
@ -13,7 +13,7 @@ Caching strategy:
"""
from datetime import datetime
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, ClassVar
from custom_components.tibber_prices.const import get_display_unit_factor
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
@ -40,14 +40,25 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
Handles three types of trend analysis:
1. Simple trends (price_trend_1h-12h): Current vs next N hours average
2. Current trend (current_price_trend): Momentum + 3h outlook with volatility adjustment
3. Next change (next_price_trend_change): Scan forward for trend reversal
2. Current trend (current_price_trend): Pure future-based 3h outlook with volatility adjustment
3. Next change (next_price_trend_change): Scan forward with configurable N-interval hysteresis (default 3)
Caching:
- Simple trends: Per-sensor cache (_cached_trend_value, _trend_attributes)
- Current/Next: Centralized cache (_trend_calculation_cache) with 60s TTL
"""
# Direction groups for trend change detection.
# Only GROUP changes count as trend changes (not intensity changes within a group).
# E.g., rising → strongly_rising is NOT a change; rising → stable IS a change.
_DIRECTION_GROUPS: ClassVar[dict[str, str]] = {
"strongly_falling": "falling",
"falling": "falling",
"stable": "stable",
"rising": "rising",
"strongly_rising": "rising",
}
def __init__(self, coordinator: "TibberPricesDataUpdateCoordinator") -> None:
"""Initialize trend calculator with caching state."""
super().__init__(coordinator)
@ -103,30 +114,51 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
return None
# Get configured thresholds from options
threshold_rising = self.config.get("price_trend_threshold_rising", 5.0)
threshold_falling = self.config.get("price_trend_threshold_falling", -5.0)
threshold_strongly_rising = self.config.get("price_trend_threshold_strongly_rising", 6.0)
threshold_strongly_falling = self.config.get("price_trend_threshold_strongly_falling", -6.0)
threshold_rising = self.config.get("price_trend_threshold_rising", 3.0)
threshold_falling = self.config.get("price_trend_threshold_falling", -3.0)
threshold_strongly_rising = self.config.get("price_trend_threshold_strongly_rising", 9.0)
threshold_strongly_falling = self.config.get("price_trend_threshold_strongly_falling", -9.0)
volatility_threshold_moderate = self.config.get("volatility_threshold_moderate", 15.0)
volatility_threshold_high = self.config.get("volatility_threshold_high", 30.0)
# Minimum absolute price change thresholds (noise floor)
# Config values are stored in base currency (EUR/NOK) - no conversion needed
min_abs_diff = self.config.get("price_trend_min_price_change", 0.005)
min_abs_diff_strongly = self.config.get("price_trend_min_price_change_strongly", 0.015)
# Prepare data for volatility-adaptive thresholds
today_prices = self.intervals_today
tomorrow_prices = self.intervals_tomorrow
all_intervals = today_prices + tomorrow_prices
lookahead_intervals = self.coordinator.time.minutes_to_intervals(hours * 60)
# Find current interval index to slice correct volatility window.
# Without this, _calculate_lookahead_volatility_factor() would analyze prices
# from the start of the day instead of the actual lookahead window.
current_idx = None
for idx, interval in enumerate(all_intervals):
if time.get_interval_time(interval) == current_starts_at:
current_idx = idx
break
if current_idx is not None:
volatility_window = all_intervals[current_idx : current_idx + lookahead_intervals]
else:
volatility_window = all_intervals[:lookahead_intervals]
# Calculate trend with volatility-adaptive thresholds
trend_state, diff_pct, trend_value = calculate_price_trend(
trend_state, diff_pct, trend_value, vol_factor = calculate_price_trend(
current_interval_price,
future_mean,
threshold_rising=threshold_rising,
threshold_falling=threshold_falling,
threshold_strongly_rising=threshold_strongly_rising,
threshold_strongly_falling=threshold_strongly_falling,
min_abs_diff=min_abs_diff,
min_abs_diff_strongly=min_abs_diff_strongly,
volatility_adjustment=True, # Always enabled
lookahead_intervals=lookahead_intervals,
all_intervals=all_intervals,
all_intervals=volatility_window,
volatility_threshold_moderate=volatility_threshold_moderate,
volatility_threshold_high=volatility_threshold_high,
)
@ -145,14 +177,19 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
factor = get_display_unit_factor(self.config_entry)
# Store attributes in sensor-specific dictionary AND cache the trend value
# Show effective thresholds (after volatility adjustment) so users can understand
# why a trend was detected even when diff_pct seems below configured thresholds
self._trend_attributes = {
"timestamp": next_interval_start,
"trend_value": trend_value,
f"trend_{hours}h_%": round(diff_pct, 1),
f"next_{hours}h_avg": round(future_mean * factor, 2),
"interval_count": lookahead_intervals,
"threshold_rising": threshold_rising,
"threshold_falling": threshold_falling,
"threshold_rising_%": round(threshold_rising * vol_factor, 1),
"threshold_rising_strongly_%": round(threshold_strongly_rising * vol_factor, 1),
"threshold_falling_%": round(threshold_falling * vol_factor, 1),
"threshold_falling_strongly_%": round(threshold_strongly_falling * vol_factor, 1),
"volatility_factor": vol_factor,
"icon_color": icon_color,
}
@ -191,9 +228,16 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
return None
# Set attributes for this sensor
# Note: "previous_direction" (not "from_direction") because this shows the
# price direction BEFORE the current trend (binary: rising/falling),
# not the trend classification. next_price_trend_change uses "from_direction"
# for the current 5-level trend state.
self._current_trend_attributes = {
"from_direction": trend_info["from_direction"],
"trend_duration_minutes": trend_info["trend_duration_minutes"],
"previous_direction": trend_info["from_direction"],
"price_direction_duration_minutes": trend_info["trend_duration_minutes"],
"price_direction_since": (
trend_info["trend_start_time"].isoformat() if trend_info["trend_start_time"] else None
),
}
return trend_info["current_trend_state"]
@ -218,6 +262,28 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
return trend_info["next_change_time"]
def get_trend_change_in_minutes_value(self) -> float | None:
"""
Calculate minutes until the next price trend change, as hours.
Returns the same data as get_next_trend_change_value() but as a duration
in minutes (converted to hours by value_getters). Shares cached attributes
with the timestamp sensor.
Returns:
Minutes until next trend change, or None if no change expected
"""
trend_info = self._calculate_trend_info()
if not trend_info:
return None
# Share attributes with the timestamp sensor
self._trend_change_attributes = trend_info["trend_change_attributes"]
return trend_info["minutes_until_change"]
def get_trend_attributes(self) -> dict[str, Any]:
"""Get cached trend attributes for simple trend sensors (price_trend_Nh)."""
return self._trend_attributes
@ -339,53 +405,26 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
# Get configured thresholds
thresholds = self._get_thresholds_config()
# Step 1: Calculate current momentum from trailing data (1h weighted)
current_price = float(current_interval["total"])
current_momentum = self._calculate_momentum(current_price, all_intervals, current_index)
# Step 1: Calculate pure future-based 3h trend (no momentum)
current_trend_state = self._calculate_standard_trend(all_intervals, current_index, current_interval, thresholds)
# Step 2: Calculate 3h baseline trend for comparison
current_trend_3h = self._calculate_standard_trend(all_intervals, current_index, current_interval, thresholds)
# Step 3: Calculate final trend FIRST (momentum + future outlook)
min_intervals_for_trend = 4
standard_lookahead = 12 # 3 hours
lookahead_intervals = standard_lookahead
# Get future data
future_intervals = all_intervals[current_index + 1 : current_index + lookahead_intervals + 1]
future_prices = [float(fi["total"]) for fi in future_intervals if "total" in fi]
# Combine momentum + future outlook to get ACTUAL current trend
if len(future_intervals) >= min_intervals_for_trend and future_prices:
future_mean = calculate_mean(future_prices)
current_trend_state = self._combine_momentum_with_future(
current_momentum=current_momentum,
current_price=current_price,
future_mean=future_mean,
context={
"all_intervals": all_intervals,
"current_index": current_index,
"lookahead_intervals": lookahead_intervals,
"thresholds": thresholds,
},
)
else:
# Not enough future data - use 3h baseline as fallback
current_trend_state = current_trend_3h
# Step 4: Find next trend change FROM the current trend state (not momentum!)
# Step 2: Find next trend change by scanning forward
scan_params = {
"current_index": current_index,
"current_trend_state": current_trend_state, # Use FINAL trend, not momentum
"current_trend_state": current_trend_state,
"current_interval": current_interval,
"now": now,
}
next_change_time = self._scan_for_trend_change(all_intervals, scan_params, thresholds)
# Step 5: Find when current trend started (scan backward)
# Step 3: Find when current trend started (scan backward)
# Use min_abs_diff as noise tolerance to ignore tiny price jitter
trend_start_time, from_direction = self._find_trend_start_time(
all_intervals, current_index, current_trend_state, thresholds
all_intervals,
current_index,
current_trend_state,
noise_tolerance=thresholds["min_abs_diff"],
)
# Calculate duration of current trend
@ -420,133 +459,17 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
def _get_thresholds_config(self) -> dict[str, float]:
"""Get configured thresholds for trend calculation."""
return {
"rising": self.config.get("price_trend_threshold_rising", 5.0),
"falling": self.config.get("price_trend_threshold_falling", -5.0),
"strongly_rising": self.config.get("price_trend_threshold_strongly_rising", 6.0),
"strongly_falling": self.config.get("price_trend_threshold_strongly_falling", -6.0),
"rising": self.config.get("price_trend_threshold_rising", 3.0),
"falling": self.config.get("price_trend_threshold_falling", -3.0),
"strongly_rising": self.config.get("price_trend_threshold_strongly_rising", 9.0),
"strongly_falling": self.config.get("price_trend_threshold_strongly_falling", -9.0),
"moderate": self.config.get("volatility_threshold_moderate", 15.0),
"high": self.config.get("volatility_threshold_high", 30.0),
# Config values are stored in base currency (EUR/NOK) - no conversion needed
"min_abs_diff": self.config.get("price_trend_min_price_change", 0.005),
"min_abs_diff_strongly": self.config.get("price_trend_min_price_change_strongly", 0.015),
}
def _calculate_momentum(self, current_price: float, all_intervals: list, current_index: int) -> str:
"""
Calculate price momentum from weighted trailing average (last 1h).
Args:
current_price: Current interval price
all_intervals: All price intervals
current_index: Index of current interval
Returns:
Momentum direction: "strongly_rising", "rising", "stable", "falling", or "strongly_falling"
"""
# Look back 1 hour (4 intervals) for quick reaction
lookback_intervals = 4
min_intervals = 2 # Need at least 30 minutes of history
trailing_intervals = all_intervals[max(0, current_index - lookback_intervals) : current_index]
if len(trailing_intervals) < min_intervals:
return "stable" # Not enough history
# Weighted average: newer intervals count more
# Weights: [0.5, 0.75, 1.0, 1.25] for 4 intervals (grows linearly)
weights = [0.5 + 0.25 * i for i in range(len(trailing_intervals))]
trailing_prices = [float(interval["total"]) for interval in trailing_intervals if "total" in interval]
if not trailing_prices or len(trailing_prices) != len(weights):
return "stable"
weighted_sum = sum(price * weight for price, weight in zip(trailing_prices, weights, strict=True))
weighted_avg = weighted_sum / sum(weights)
# Calculate momentum with thresholds
# Using same logic as 5-level trend: 3% for normal, 6% (2x) for strong
momentum_threshold = 0.03
strong_momentum_threshold = 0.06
diff = (current_price - weighted_avg) / abs(weighted_avg) if weighted_avg != 0 else 0
# Determine momentum level based on thresholds
if diff >= strong_momentum_threshold:
momentum = "strongly_rising"
elif diff > momentum_threshold:
momentum = "rising"
elif diff <= -strong_momentum_threshold:
momentum = "strongly_falling"
elif diff < -momentum_threshold:
momentum = "falling"
else:
momentum = "stable"
return momentum
def _combine_momentum_with_future(
self,
*,
current_momentum: str,
current_price: float,
future_mean: float,
context: dict,
) -> str:
"""
Combine momentum analysis with future outlook to determine final trend.
Uses 5-level scale: strongly_rising, rising, stable, falling, strongly_falling.
Momentum intensity is preserved when future confirms the trend direction.
Args:
current_momentum: Current momentum direction (5-level scale)
current_price: Current interval price
future_mean: Average price in future window
context: Dict with all_intervals, current_index, lookahead_intervals, thresholds
Returns:
Final trend direction (5-level scale)
"""
# Use calculate_price_trend for consistency with 5-level logic
all_intervals = context["all_intervals"]
current_index = context["current_index"]
lookahead_intervals = context["lookahead_intervals"]
thresholds = context["thresholds"]
lookahead_for_volatility = all_intervals[current_index : current_index + lookahead_intervals]
future_trend, _, _ = calculate_price_trend(
current_price,
future_mean,
threshold_rising=thresholds["rising"],
threshold_falling=thresholds["falling"],
threshold_strongly_rising=thresholds["strongly_rising"],
threshold_strongly_falling=thresholds["strongly_falling"],
volatility_adjustment=True,
lookahead_intervals=lookahead_intervals,
all_intervals=lookahead_for_volatility,
volatility_threshold_moderate=thresholds["moderate"],
volatility_threshold_high=thresholds["high"],
)
# Check if momentum and future trend are aligned (same direction)
momentum_rising = current_momentum in ("rising", "strongly_rising")
momentum_falling = current_momentum in ("falling", "strongly_falling")
future_rising = future_trend in ("rising", "strongly_rising")
future_falling = future_trend in ("falling", "strongly_falling")
if momentum_rising and future_rising:
# Both indicate rising - use the stronger signal
if current_momentum == "strongly_rising" or future_trend == "strongly_rising":
return "strongly_rising"
return "rising"
if momentum_falling and future_falling:
# Both indicate falling - use the stronger signal
if current_momentum == "strongly_falling" or future_trend == "strongly_falling":
return "strongly_falling"
return "falling"
# Conflicting signals or stable momentum - trust future trend calculation
return future_trend
def _calculate_standard_trend(
self,
all_intervals: list,
@ -571,13 +494,15 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
current_price = float(current_interval["total"])
standard_lookahead_volatility = all_intervals[current_index : current_index + standard_lookahead]
current_trend_3h, _, _ = calculate_price_trend(
current_trend_3h, _, _, _ = calculate_price_trend(
current_price,
standard_future_mean,
threshold_rising=thresholds["rising"],
threshold_falling=thresholds["falling"],
threshold_strongly_rising=thresholds["strongly_rising"],
threshold_strongly_falling=thresholds["strongly_falling"],
min_abs_diff=thresholds["min_abs_diff"],
min_abs_diff_strongly=thresholds["min_abs_diff_strongly"],
volatility_adjustment=True,
lookahead_intervals=standard_lookahead,
all_intervals=standard_lookahead_volatility,
@ -601,73 +526,91 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
all_intervals: list,
current_index: int,
current_trend_state: str,
thresholds: dict,
*,
noise_tolerance: float = 0.0,
) -> tuple[datetime | None, str | None]:
"""
Find when the current trend started by scanning backward.
Find when the current trend started by scanning backward for price direction change.
Uses lightweight price comparison instead of recalculating full trend at each
past interval. The trend start is where the price direction changed i.e., where
prices stopped moving in the current direction and started moving the other way.
Price changes smaller than noise_tolerance (in base currency, e.g. EUR) are
ignored. This prevents tiny jitter (e.g. 0.1ct fluctuation in a 10ct20ct
uptrend) from cutting the detected trend duration short.
For "stable" trends, the start is where prices stopped rising or falling.
Args:
all_intervals: List of all price intervals
current_index: Index of current interval
current_trend_state: Current trend state ("rising", "falling", "stable")
thresholds: Threshold configuration
current_trend_state: Current trend state (e.g., "rising", "falling", "stable")
noise_tolerance: Minimum absolute price change (base currency) to count as
a direction change. Defaults to 0.0 (no tolerance).
Returns:
Tuple of (start_time, from_direction):
- start_time: When current trend began, or None if at data boundary
- from_direction: Previous trend direction, or None if unknown
- from_direction: Previous price direction, or None if unknown
"""
intervals_in_3h = 12 # 3 hours = 12 intervals @ 15min each
# Scan backward to find when trend changed TO current state
time = self.coordinator.time
for i in range(current_index - 1, max(-1, current_index - 97), -1):
max_lookback = 97 # ~24h
# Map current trend to expected price direction
is_rising = current_trend_state in ("rising", "strongly_rising")
is_falling = current_trend_state in ("falling", "strongly_falling")
# Scan backward looking for where price direction changed
prev_price = float(all_intervals[current_index]["total"]) if "total" in all_intervals[current_index] else None
if prev_price is None:
return None, None
for i in range(current_index - 1, max(-1, current_index - max_lookback), -1):
if i < 0:
break
interval = all_intervals[i]
price = float(interval["total"]) if "total" in interval else None
if price is None:
continue
interval_start = time.get_interval_time(interval)
if not interval_start:
continue
# Calculate trend at this past interval
future_intervals = all_intervals[i + 1 : i + intervals_in_3h + 1]
if len(future_intervals) < intervals_in_3h:
break # Not enough data to calculate trend
# Calculate signed price difference: positive = price was rising, negative = falling
price_diff = prev_price - price
future_prices = [float(fi["total"]) for fi in future_intervals if "total" in fi]
if not future_prices:
continue
# Apply noise tolerance: ignore price changes below threshold
direction_was_rising = price_diff > noise_tolerance
direction_was_falling = price_diff < -noise_tolerance
# If |price_diff| <= noise_tolerance → neither → continue scanning
future_mean = calculate_mean(future_prices)
price = float(interval["total"])
# Calculate trend at this past point
lookahead_for_volatility = all_intervals[i : i + intervals_in_3h]
trend_state, _, _ = calculate_price_trend(
price,
future_mean,
threshold_rising=thresholds["rising"],
threshold_falling=thresholds["falling"],
threshold_strongly_rising=thresholds["strongly_rising"],
threshold_strongly_falling=thresholds["strongly_falling"],
volatility_adjustment=True,
lookahead_intervals=intervals_in_3h,
all_intervals=lookahead_for_volatility,
volatility_threshold_moderate=thresholds["moderate"],
volatility_threshold_high=thresholds["high"],
)
# Check if trend was different from current trend state
if trend_state != current_trend_state:
# Found the change point - the NEXT interval is where current trend started
# Check if direction contradicts current trend
if is_rising and direction_was_falling:
# Price was falling here, but we're currently rising → trend started at next interval
next_interval = all_intervals[i + 1]
trend_start = time.get_interval_time(next_interval)
if trend_start:
return trend_start, trend_state
return trend_start, "falling"
# Reached data boundary - current trend extends beyond available data
if is_falling and direction_was_rising:
# Price was rising here, but we're currently falling → trend started at next interval
next_interval = all_intervals[i + 1]
trend_start = time.get_interval_time(next_interval)
return trend_start, "rising"
if not is_rising and not is_falling and (direction_was_rising or direction_was_falling):
# Current trend is "stable" — look for any clear directional movement
next_interval = all_intervals[i + 1]
trend_start = time.get_interval_time(next_interval)
from_dir = "rising" if direction_was_rising else "falling"
return trend_start, from_dir
prev_price = price
# Reached data boundary
return None, None
def _scan_for_trend_change(
@ -677,7 +620,11 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
thresholds: dict,
) -> datetime | None:
"""
Scan future intervals for trend change.
Scan future intervals for trend change with hysteresis.
Requires N consecutive intervals (configurable, default 3) showing a different
trend before confirming a change. This prevents false positives from short-lived
price spikes.
Args:
all_intervals: List of all price intervals
@ -685,16 +632,31 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
thresholds: Dict with rising, falling, moderate, high threshold values
Returns:
Timestamp of next trend change, or None if no change in next 24h
Timestamp of first interval of confirmed trend change, or None if no change
"""
time = self.coordinator.time
intervals_in_3h = 12 # 3 hours = 12 intervals @ 15min each
required_consecutive = int(self.config.get("price_trend_change_confirmation", 3))
# Reset attributes to prevent stale data from previous calculation.
# Without this, old attributes persist when no trend change is found,
# causing the sensor to show state=unknown with misleading old values.
self._trend_change_attributes = None
current_index = scan_params["current_index"]
current_trend_state = scan_params["current_trend_state"]
current_interval = scan_params["current_interval"]
now = scan_params["now"]
# Use direction groups: only group changes count as trend changes.
# rising/strongly_rising → "rising", falling/strongly_falling → "falling", stable → "stable"
current_group = self._DIRECTION_GROUPS.get(current_trend_state, "stable")
# Track consecutive intervals with different trend direction group
consecutive_different = 0
first_change: dict[str, Any] | None = None # {index, trend, mean, diff, vol_factor}
for i in range(current_index + 1, min(current_index + 97, len(all_intervals))):
interval = all_intervals[i]
interval_start = time.get_interval_time(interval)
@ -719,13 +681,15 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
# Calculate trend at this future point
lookahead_for_volatility = all_intervals[i : i + intervals_in_3h]
trend_state, _, _ = calculate_price_trend(
trend_state, trend_diff, _, vol_factor = calculate_price_trend(
current_price,
future_mean,
threshold_rising=thresholds["rising"],
threshold_falling=thresholds["falling"],
threshold_strongly_rising=thresholds["strongly_rising"],
threshold_strongly_falling=thresholds["strongly_falling"],
min_abs_diff=thresholds["min_abs_diff"],
min_abs_diff_strongly=thresholds["min_abs_diff_strongly"],
volatility_adjustment=True,
lookahead_intervals=intervals_in_3h,
all_intervals=lookahead_for_volatility,
@ -733,25 +697,50 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator):
volatility_threshold_high=thresholds["high"],
)
# Check if trend changed from current trend state
# We want to find ANY change from current state, including changes to/from stable
if trend_state != current_trend_state:
# Store details for attributes
time = self.coordinator.time
minutes_until = int(time.minutes_until(interval_start))
new_group = self._DIRECTION_GROUPS.get(trend_state, "stable")
# Convert prices to display currency unit
factor = get_display_unit_factor(self.config_entry)
if new_group != current_group:
consecutive_different += 1
if consecutive_different == 1:
# Remember the first different interval (5-level state for attributes)
first_change = {
"index": i,
"trend": trend_state,
"mean": future_mean,
"diff": trend_diff,
"vol_factor": vol_factor,
}
self._trend_change_attributes = {
"direction": trend_state,
"from_direction": current_trend_state,
"minutes_until_change": minutes_until,
"current_price_now": round(float(current_interval["total"]) * factor, 2),
"price_at_change": round(current_price * factor, 2),
"avg_after_change": round(future_mean * factor, 2),
"trend_diff_%": round((future_mean - current_price) / current_price * 100, 1),
}
return interval_start
if consecutive_different >= required_consecutive and first_change is not None:
# Confirmed: N consecutive intervals show different trend direction
change_interval = all_intervals[first_change["index"]]
change_time = time.get_interval_time(change_interval)
if change_time:
change_price = float(change_interval["total"])
minutes_until = int(time.minutes_until(change_time))
factor = get_display_unit_factor(self.config_entry)
vf = first_change["vol_factor"]
self._trend_change_attributes = {
"direction": first_change["trend"],
"from_direction": current_trend_state,
"minutes_until_change": minutes_until,
"price_now": round(float(current_interval["total"]) * factor, 2),
"price_at_change": round(change_price * factor, 2),
"price_avg_after_change": (
round(first_change["mean"] * factor, 2) if first_change["mean"] else None
),
"trend_diff_%": round(first_change["diff"], 1),
"threshold_rising_%": round(thresholds["rising"] * vf, 1),
"threshold_rising_strongly_%": round(thresholds["strongly_rising"] * vf, 1),
"threshold_falling_%": round(thresholds["falling"] * vf, 1),
"threshold_falling_strongly_%": round(thresholds["strongly_falling"] * vf, 1),
"volatility_factor": vf,
}
return change_time
else:
# Reset counter — trend matches current again
consecutive_different = 0
first_change = None
return None

View file

@ -120,6 +120,22 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
"cache_validity",
"data_completeness",
"data_status",
"threshold_rising_%",
"threshold_rising_strongly_%",
"threshold_falling_%",
"threshold_falling_strongly_%",
"volatility_factor",
"interval_count",
"price_direction_since",
"price_now",
"trend_diff_%",
# Dynamic keys for second_half diff (all trend hour variants)
"second_half_3h_diff_from_current_%",
"second_half_4h_diff_from_current_%",
"second_half_5h_diff_from_current_%",
"second_half_6h_diff_from_current_%",
"second_half_8h_diff_from_current_%",
"second_half_12h_diff_from_current_%",
# Static/Rarely Changing
"tomorrow_expected_after",
"level_value",
@ -313,7 +329,11 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
if self.entity_description.key.startswith("price_trend_"):
self._trend_calculator.clear_trend_cache()
# Clear trend calculation cache for trend sensors
elif self.entity_description.key in ("current_price_trend", "next_price_trend_change"):
elif self.entity_description.key in (
"current_price_trend",
"next_price_trend_change",
"trend_change_in_minutes",
):
self._trend_calculator.clear_calculation_cache()
# For lifecycle sensor: Only write state if it actually changed (state-change filter)

View file

@ -517,6 +517,17 @@ FUTURE_TREND_SENSORS = (
state_class=None, # Timestamp: no statistics
entity_registry_enabled_default=True,
),
# Trend change countdown sensor (how long until trend changes?)
SensorEntityDescription(
key="trend_change_in_minutes",
translation_key="trend_change_in_minutes",
icon="mdi:timer-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Countdown timer: no statistics
suggested_display_precision=2,
entity_registry_enabled_default=True,
),
# Price trend forecast sensors (will prices be higher/lower in X hours?)
# Default enabled: 1h-5h
SensorEntityDescription(

View file

@ -205,6 +205,7 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
# Current and next trend change sensors
"current_price_trend": trend_calculator.get_current_trend_value,
"next_price_trend_change": trend_calculator.get_next_trend_change_value,
"trend_change_in_minutes": lambda: _minutes_to_hours(trend_calculator.get_trend_change_in_minutes_value()),
# Price trend sensors
"price_trend_1h": lambda: trend_calculator.get_price_trend_value(hours=1),
"price_trend_2h": lambda: trend_calculator.get_price_trend_value(hours=2),

View file

@ -304,13 +304,19 @@
"price_trend_threshold_rising": "Steigend-Schwelle",
"price_trend_threshold_strongly_rising": "Stark steigend-Schwelle",
"price_trend_threshold_falling": "Fallend-Schwelle",
"price_trend_threshold_strongly_falling": "Stark fallend-Schwelle"
"price_trend_threshold_strongly_falling": "Stark fallend-Schwelle",
"price_trend_change_confirmation": "Trendwechsel-Bestätigung",
"price_trend_min_price_change": "Mind. Preisänderung (Trend)",
"price_trend_min_price_change_strongly": "Mind. Preisänderung (Starker Trend)"
},
"data_description": {
"price_trend_threshold_rising": "Prozentwert, um wie viel der Durchschnitt der nächsten N Stunden über dem aktuellen Preis liegen muss, damit der Trend als 'steigend' gilt. Beispiel: 3 bedeutet Durchschnitt ist mindestens 3% höher → Preise werden steigen. Typische Werte: 3-10%. Standard: 3%",
"price_trend_threshold_strongly_rising": "Prozentwert für 'stark steigend'-Trend. Muss höher sein als die steigend-Schwelle. Beispiel: 6 bedeutet Durchschnitt ist mindestens 6% höher → Preise werden deutlich steigen. Typische Werte: 6-15%. Standard: 6%",
"price_trend_threshold_strongly_rising": "Prozentwert für 'stark steigend'-Trend. Muss höher sein als die steigend-Schwelle. Beispiel: 9 bedeutet Durchschnitt ist mindestens 9% höher → Preise werden deutlich steigen. Typische Werte: 6-20%. Standard: 9%",
"price_trend_threshold_falling": "Prozentwert (negativ), um wie viel der Durchschnitt der nächsten N Stunden unter dem aktuellen Preis liegen muss, damit der Trend als 'fallend' gilt. Beispiel: -3 bedeutet Durchschnitt ist mindestens 3% niedriger → Preise werden fallen. Typische Werte: -3 bis -10%. Standard: -3%",
"price_trend_threshold_strongly_falling": "Prozentwert (negativ) für 'stark fallend'-Trend. Muss niedriger (negativer) sein als die fallend-Schwelle. Beispiel: -6 bedeutet Durchschnitt ist mindestens 6% niedriger → Preise werden deutlich fallen. Typische Werte: -6 bis -15%. Standard: -6%"
"price_trend_threshold_strongly_falling": "Prozentwert (negativ) für 'stark fallend'-Trend. Muss niedriger (negativer) sein als die fallend-Schwelle. Beispiel: -9 bedeutet Durchschnitt ist mindestens 9% niedriger → Preise werden deutlich fallen. Typische Werte: -6 bis -20%. Standard: -9%",
"price_trend_change_confirmation": "Anzahl aufeinanderfolgender 15-Minuten-Intervalle, die eine neue Trendrichtung bestätigen müssen, bevor ein Trendwechsel gemeldet wird. Höhere Werte bedeuten mehr Stabilität und weniger Fehlsignale, niedrigere Werte bedeuten schnellere Erkennung. Bereich: 2 (30 min) bis 6 (90 min). Standard: 3 (45 min)",
"price_trend_min_price_change": "Mindest-Preisdifferenz (in ct/øre), die für einen 'steigend'- oder 'fallend'-Trend erforderlich ist. Verhindert, dass minimale Preisänderungen bei niedrigem Preisniveau Trends auslösen. Auf 0 setzen zum Deaktivieren (reiner Prozentmodus). Standard: 0,5",
"price_trend_min_price_change_strongly": "Mindest-Preisdifferenz (in ct/øre), die für einen 'stark steigend'- oder 'stark fallend'-Trend erforderlich ist. Sollte höher als das reguläre Trend-Minimum sein. Auf 0 setzen zum Deaktivieren. Standard: 1,5"
},
"submit": "↩ Speichern & Zurück"
},
@ -706,6 +712,9 @@
"next_price_trend_change": {
"name": "Nächste Trendänderung"
},
"trend_change_in_minutes": {
"name": "Trendänderung in"
},
"daily_rating": {
"name": "Tägliche Preisbewertung"
},

View file

@ -315,13 +315,19 @@
"price_trend_threshold_rising": "Rising Threshold",
"price_trend_threshold_strongly_rising": "Strongly Rising Threshold",
"price_trend_threshold_falling": "Falling Threshold",
"price_trend_threshold_strongly_falling": "Strongly Falling Threshold"
"price_trend_threshold_strongly_falling": "Strongly Falling Threshold",
"price_trend_change_confirmation": "Trend Change Confirmation",
"price_trend_min_price_change": "Min. Price Change (Trend)",
"price_trend_min_price_change_strongly": "Min. Price Change (Strong Trend)"
},
"data_description": {
"price_trend_threshold_rising": "Percentage that the average of the next N hours must be above the current price to qualify as 'rising' trend. Example: 3 means average is at least 3% higher → prices will rise. Typical values: 3-10%. Default: 3%",
"price_trend_threshold_strongly_rising": "Percentage for 'strongly rising' trend. Must be higher than rising threshold. Example: 6 means average is at least 6% higher → prices will rise significantly. Typical values: 6-15%. Default: 6%",
"price_trend_threshold_strongly_rising": "Percentage for 'strongly rising' trend. Must be higher than rising threshold. Example: 9 means average is at least 9% higher → prices will rise significantly. Typical values: 6-20%. Default: 9%",
"price_trend_threshold_falling": "Percentage (negative) that the average of the next N hours must be below the current price to qualify as 'falling' trend. Example: -3 means average is at least 3% lower → prices will fall. Typical values: -3 to -10%. Default: -3%",
"price_trend_threshold_strongly_falling": "Percentage (negative) for 'strongly falling' trend. Must be lower (more negative) than falling threshold. Example: -6 means average is at least 6% lower → prices will fall significantly. Typical values: -6 to -15%. Default: -6%"
"price_trend_threshold_strongly_falling": "Percentage (negative) for 'strongly falling' trend. Must be lower (more negative) than falling threshold. Example: -9 means average is at least 9% lower → prices will fall significantly. Typical values: -6 to -20%. Default: -9%",
"price_trend_change_confirmation": "Number of consecutive 15-minute intervals that must confirm a new trend direction before reporting a trend change. Higher values mean more stability and fewer false changes, lower values mean faster detection. Range: 2 (30 min) to 6 (90 min). Default: 3 (45 min)",
"price_trend_min_price_change": "Minimum absolute price difference (in ct/øre) required for a 'rising' or 'falling' trend. Prevents tiny price changes from triggering trends at low price levels. Set to 0 to disable (pure percentage mode). Default: 0.5",
"price_trend_min_price_change_strongly": "Minimum absolute price difference (in ct/øre) required for a 'strongly rising' or 'strongly falling' trend. Should be higher than the regular trend minimum. Set to 0 to disable. Default: 1.5"
},
"submit": "↩ Save & Back"
},
@ -706,6 +712,9 @@
"next_price_trend_change": {
"name": "Next Price Trend Change"
},
"trend_change_in_minutes": {
"name": "Trend Change In"
},
"daily_rating": {
"name": "Daily Price Rating"
},

View file

@ -304,13 +304,19 @@
"price_trend_threshold_rising": "Stigende terskel",
"price_trend_threshold_strongly_rising": "Sterkt stigende terskel",
"price_trend_threshold_falling": "Fallende terskel",
"price_trend_threshold_strongly_falling": "Sterkt fallende terskel"
"price_trend_threshold_strongly_falling": "Sterkt fallende terskel",
"price_trend_change_confirmation": "Trendendringsbekreftelse",
"price_trend_min_price_change": "Min. prisendring (trend)",
"price_trend_min_price_change_strongly": "Min. prisendring (sterk trend)"
},
"data_description": {
"price_trend_threshold_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'stigende' trend. Eksempel: 3 betyr gjennomsnittet er minst 3% høyere → prisene vil stige. Typiske verdier: 3-10%. Standard: 3%",
"price_trend_threshold_strongly_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'sterkt stigende' trend. Må være høyere enn stigende terskel. Typiske verdier: 6-20%. Standard: 6%",
"price_trend_threshold_strongly_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'sterkt stigende' trend. Må være høyere enn stigende terskel. Typiske verdier: 6-20%. Standard: 9%",
"price_trend_threshold_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'synkende' trend. Eksempel: -3 betyr gjennomsnittet er minst 3% lavere → prisene vil falle. Typiske verdier: -3 til -10%. Standard: -3%",
"price_trend_threshold_strongly_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'sterkt synkende' trend. Må være lavere (mer negativ) enn fallende terskel. Typiske verdier: -6 til -20%. Standard: -6%"
"price_trend_threshold_strongly_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'sterkt synkende' trend. Må være lavere (mer negativ) enn fallende terskel. Typiske verdier: -6 til -20%. Standard: -9%",
"price_trend_change_confirmation": "Antall påfølgende 15-minutters intervaller som må bekrefte en ny trendretning før en trendendring rapporteres. Høyere verdier betyr mer stabilitet og færre falske endringer, lavere verdier betyr raskere oppdagelse. Område: 2 (30 min) til 6 (90 min). Standard: 3 (45 min)",
"price_trend_min_price_change": "Minste absolutte prisdifferanse (i ct/øre) som kreves for en 'stigende' eller 'synkende' trend. Forhindrer at minimale prisendringer utløser trender ved lave prisnivåer. Sett til 0 for å deaktivere (ren prosentmodus). Standard: 0,5",
"price_trend_min_price_change_strongly": "Minste absolutte prisdifferanse (i ct/øre) som kreves for en 'sterkt stigende' eller 'sterkt synkende' trend. Bør være høyere enn det vanlige trendminimum. Sett til 0 for å deaktivere. Standard: 1,5"
},
"submit": "↩ Lagre & tilbake"
},
@ -706,6 +712,9 @@
"next_price_trend_change": {
"name": "Neste trendendring"
},
"trend_change_in_minutes": {
"name": "Trendendring om"
},
"daily_rating": {
"name": "Daglig prisvurdering"
},

View file

@ -304,13 +304,19 @@
"price_trend_threshold_rising": "Stijgende Drempel",
"price_trend_threshold_strongly_rising": "Sterk Stijgende Drempel",
"price_trend_threshold_falling": "Dalende Drempel",
"price_trend_threshold_strongly_falling": "Sterk Dalende Drempel"
"price_trend_threshold_strongly_falling": "Sterk Dalende Drempel",
"price_trend_change_confirmation": "Trendverandering Bevestiging",
"price_trend_min_price_change": "Min. Prijsverandering (Trend)",
"price_trend_min_price_change_strongly": "Min. Prijsverandering (Sterke Trend)"
},
"data_description": {
"price_trend_threshold_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'stijgende' trend. Voorbeeld: 3 betekent dat het gemiddelde minimaal 3% hoger is → prijzen zullen stijgen. Typische waarden: 3-10%. Standaard: 3%",
"price_trend_threshold_strongly_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'sterk stijgende' trend. Moet hoger zijn dan stijgende drempel. Typische waarden: 6-20%. Standaard: 6%",
"price_trend_threshold_strongly_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'sterk stijgende' trend. Moet hoger zijn dan stijgende drempel. Typische waarden: 6-20%. Standaard: 9%",
"price_trend_threshold_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'dalende' trend. Voorbeeld: -3 betekent dat het gemiddelde minimaal 3% lager is → prijzen zullen dalen. Typische waarden: -3 tot -10%. Standaard: -3%",
"price_trend_threshold_strongly_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'sterk dalende' trend. Moet lager (meer negatief) zijn dan dalende drempel. Typische waarden: -6 tot -20%. Standaard: -6%"
"price_trend_threshold_strongly_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'sterk dalende' trend. Moet lager (meer negatief) zijn dan dalende drempel. Typische waarden: -6 tot -20%. Standaard: -9%",
"price_trend_change_confirmation": "Aantal opeenvolgende 15-minuten intervallen dat een nieuwe trendrichting moet bevestigen voordat een trendverandering wordt gemeld. Hogere waarden betekenen meer stabiliteit en minder valse veranderingen, lagere waarden betekenen snellere detectie. Bereik: 2 (30 min) tot 6 (90 min). Standaard: 3 (45 min)",
"price_trend_min_price_change": "Minimaal absoluut prijsverschil (in ct/øre) vereist voor een 'stijgende' of 'dalende' trend. Voorkomt dat minimale prijswijzigingen trends veroorzaken bij lage prijsniveaus. Stel in op 0 om te deactiveren (pure percentagemodus). Standaard: 0,5",
"price_trend_min_price_change_strongly": "Minimaal absoluut prijsverschil (in ct/øre) vereist voor een 'sterk stijgende' of 'sterk dalende' trend. Moet hoger zijn dan het reguliere trendminimum. Stel in op 0 om te deactiveren. Standaard: 1,5"
},
"submit": "↩ Opslaan & Terug"
},
@ -706,6 +712,9 @@
"next_price_trend_change": {
"name": "Volgende Prijstrend Wijziging"
},
"trend_change_in_minutes": {
"name": "Trendwijziging over"
},
"daily_rating": {
"name": "Dagelijkse Prijsbeoordeling"
},

View file

@ -304,13 +304,19 @@
"price_trend_threshold_rising": "Stigande tröskel",
"price_trend_threshold_strongly_rising": "Kraftigt stigande tröskel",
"price_trend_threshold_falling": "Fallande tröskel",
"price_trend_threshold_strongly_falling": "Kraftigt fallande tröskel"
"price_trend_threshold_strongly_falling": "Kraftigt fallande tröskel",
"price_trend_change_confirmation": "Trendändringsbekräftelse",
"price_trend_min_price_change": "Min. prisändring (trend)",
"price_trend_min_price_change_strongly": "Min. prisändring (stark trend)"
},
"data_description": {
"price_trend_threshold_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'stigande' trend. Exempel: 3 betyder att genomsnittet är minst 3% högre → priserna kommer att stiga. Typiska värden: 3-10%. Standard: 3%",
"price_trend_threshold_strongly_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'kraftigt stigande' trend. Måste vara högre än stigande tröskel. Typiska värden: 6-20%. Standard: 6%",
"price_trend_threshold_strongly_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'kraftigt stigande' trend. Måste vara högre än stigande tröskel. Typiska värden: 6-20%. Standard: 9%",
"price_trend_threshold_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'fallande' trend. Exempel: -3 betyder att genomsnittet är minst 3% lägre → priserna kommer att falla. Typiska värden: -3 till -10%. Standard: -3%",
"price_trend_threshold_strongly_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'kraftigt fallande' trend. Måste vara lägre (mer negativ) än fallande tröskel. Typiska värden: -6 till -20%. Standard: -6%"
"price_trend_threshold_strongly_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'kraftigt fallande' trend. Måste vara lägre (mer negativ) än fallande tröskel. Typiska värden: -6 till -20%. Standard: -9%",
"price_trend_change_confirmation": "Antal på varandra följande 15-minutersintervall som måste bekräfta en ny trendriktning innan en trendändring rapporteras. Högre värden innebär mer stabilitet och färre falska ändringar, lägre värden innebär snabbare upptäckt. Intervall: 2 (30 min) till 6 (90 min). Standard: 3 (45 min)",
"price_trend_min_price_change": "Minsta absoluta prisskillnad (i ct/öre) som krävs för en 'stigande' eller 'fallande' trend. Förhindrar att minimala prisändringar utlöser trender vid låga prisnivåer. Sätt till 0 för att inaktivera (rent procentläge). Standard: 0,5",
"price_trend_min_price_change_strongly": "Minsta absoluta prisskillnad (i ct/öre) som krävs för en 'kraftigt stigande' eller 'kraftigt fallande' trend. Bör vara högre än det vanliga trendminimumet. Sätt till 0 för att inaktivera. Standard: 1,5"
},
"submit": "↩ Spara & tillbaka"
},
@ -706,6 +712,9 @@
"next_price_trend_change": {
"name": "Nästa pristrendändring"
},
"trend_change_in_minutes": {
"name": "Trendändring om"
},
"daily_rating": {
"name": "Dagligt prisbetyg"
},

View file

@ -1136,14 +1136,16 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
threshold_rising: float = 3.0,
threshold_falling: float = -3.0,
*,
threshold_strongly_rising: float = 6.0,
threshold_strongly_falling: float = -6.0,
threshold_strongly_rising: float = 9.0,
threshold_strongly_falling: float = -9.0,
min_abs_diff: float = 0.0,
min_abs_diff_strongly: float = 0.0,
volatility_adjustment: bool = True,
lookahead_intervals: int | None = None,
all_intervals: list[dict[str, Any]] | None = None,
volatility_threshold_moderate: float = DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
volatility_threshold_high: float = DEFAULT_VOLATILITY_THRESHOLD_HIGH,
) -> tuple[str, float, int]:
) -> tuple[str, float, int, float]:
"""
Calculate price trend by comparing current price with future average.
@ -1165,6 +1167,10 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
Uses the same volatility thresholds as configured for volatility sensors,
ensuring consistent volatility interpretation across the integration.
Additionally supports minimum absolute price difference thresholds (noise floor)
to prevent tiny absolute changes from triggering trends at low price levels.
Both percentage AND absolute conditions must be met for a trend to be detected.
Args:
current_interval_price: Current interval price
future_average: Average price of future intervals
@ -1172,6 +1178,8 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
threshold_falling: Base threshold for falling trend (%, negative, default -3%)
threshold_strongly_rising: Threshold for strongly rising (%, positive, default 6%)
threshold_strongly_falling: Threshold for strongly falling (%, negative, default -6%)
min_abs_diff: Minimum absolute price difference for rising/falling (base currency, 0=disabled)
min_abs_diff_strongly: Minimum absolute price difference for strongly rising/falling (base currency, 0=disabled)
volatility_adjustment: Enable volatility-adaptive thresholds (default True)
lookahead_intervals: Number of intervals in trend period for volatility calc
all_intervals: Price intervals (today + tomorrow) for volatility calculation
@ -1179,10 +1187,11 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
volatility_threshold_high: User-configured high volatility threshold (%)
Returns:
Tuple of (trend_state, difference_percentage, trend_value)
Tuple of (trend_state, difference_percentage, trend_value, volatility_factor)
trend_state: PRICE_TREND_* constant (e.g., "strongly_rising")
difference_percentage: % change from current to future ((future - current) / current * 100)
trend_value: Integer value from -2 to +2 for automation comparisons
volatility_factor: Applied multiplier (0.6/1.0/1.4) for threshold transparency
Note:
Volatility adjustment factor:
@ -1193,9 +1202,10 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
"""
if current_interval_price == 0:
# Avoid division by zero - return stable trend
return PRICE_TREND_STABLE, 0.0, PRICE_TREND_MAPPING[PRICE_TREND_STABLE]
return PRICE_TREND_STABLE, 0.0, PRICE_TREND_MAPPING[PRICE_TREND_STABLE], 1.0
# Apply volatility adjustment if enabled and data available
volatility_factor = 1.0
effective_rising = threshold_rising
effective_falling = threshold_falling
effective_strongly_rising = threshold_strongly_rising
@ -1215,17 +1225,21 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v
# Example: current=-10, future=-5 → diff=5, pct=5/abs(-10)*100=+50% (correctly shows rising)
diff_pct = ((future_average - current_interval_price) / abs(current_interval_price)) * 100
# Calculate absolute price difference (for noise floor check)
abs_diff = abs(future_average - current_interval_price)
# Determine trend based on effective thresholds (5-level scale)
# Check "strongly" conditions first (more extreme), then regular conditions
if diff_pct >= effective_strongly_rising:
# Both percentage AND absolute minimum conditions must be met.
# This prevents tiny absolute changes (e.g., 0.15 ct at 5 ct/kWh) from triggering trends.
if diff_pct >= effective_strongly_rising and abs_diff >= min_abs_diff_strongly:
trend = PRICE_TREND_STRONGLY_RISING
elif diff_pct >= effective_rising:
elif diff_pct >= effective_rising and abs_diff >= min_abs_diff:
trend = PRICE_TREND_RISING
elif diff_pct <= effective_strongly_falling:
elif diff_pct <= effective_strongly_falling and abs_diff >= min_abs_diff_strongly:
trend = PRICE_TREND_STRONGLY_FALLING
elif diff_pct <= effective_falling:
elif diff_pct <= effective_falling and abs_diff >= min_abs_diff:
trend = PRICE_TREND_FALLING
else:
trend = PRICE_TREND_STABLE
return trend, diff_pct, PRICE_TREND_MAPPING[trend]
return trend, diff_pct, PRICE_TREND_MAPPING[trend], volatility_factor

File diff suppressed because it is too large Load diff

View file

@ -1,49 +1,50 @@
{
"name": "docs-split-developer",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.9.2",
"@docusaurus/preset-classic": "3.9.2",
"@docusaurus/theme-mermaid": "^3.9.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.6.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.9.2",
"typescript": "~5.6.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=20.0"
}
"name": "docs-split-developer",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^3.10.0",
"@docusaurus/faster": "^3.10.0",
"@docusaurus/preset-classic": "^3.10.0",
"@docusaurus/theme-mermaid": "^3.10.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.6.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.0",
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/types": "^3.10.0",
"typescript": "~6.0.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=20.0"
}
}

View file

@ -0,0 +1,186 @@
---
comments: false
---
# API Reference
Documentation of the Tibber GraphQL API used by this integration.
## GraphQL Endpoint
```
https://api.tibber.com/v1-beta/gql
```
**Authentication:** Bearer token in `Authorization` header
## Queries Used
### User Data Query
Fetches home information and metadata:
```graphql
query {
viewer {
homes {
id
appNickname
address {
address1
postalCode
city
country
}
timeZone
currentSubscription {
priceInfo {
current {
currency
}
}
}
meteringPointData {
consumptionEan
gridAreaCode
}
}
}
}
```
**Cached for:** 24 hours
### Price Data 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
}
}
}
}
}
}
}
```
**Parameters:**
- `homeId`: Tibber home identifier
- `resolution`: Always `QUARTER_HOURLY`
- `first`: 384 intervals (4 days of data)
**Cached until:** Midnight local time
## 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
## Response Format
### Price Node Structure
```json
{
"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)
### Currency Information
```json
{
"currency": "EUR"
}
```
Supported currencies:
- `EUR` (Euro) - displayed as ct/kWh
- `NOK` (Norwegian Krone) - displayed as øre/kWh
- `SEK` (Swedish Krona) - displayed as öre/kWh
## Error Handling
### Common Error Responses
**Invalid Token:**
```json
{
"errors": [{
"message": "Unauthorized",
"extensions": {
"code": "UNAUTHENTICATED"
}
}]
}
```
**Rate Limit Exceeded:**
```json
{
"errors": [{
"message": "Too Many Requests",
"extensions": {
"code": "RATE_LIMIT_EXCEEDED"
}
}]
}
```
**Home Not Found:**
```json
{
"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
## 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
- **Custom rating** - Based on user thresholds (different from Tibber's `level`)
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)

View file

@ -0,0 +1,358 @@
---
comments: false
---
# Architecture
This document provides a visual overview of the integration's architecture, focusing on end-to-end data flow and caching layers.
For detailed implementation patterns, see [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md).
---
## End-to-End Data Flow
```mermaid
flowchart TB
%% External Systems
TIBBER[("🌐 Tibber GraphQL API<br/>api.tibber.com")]
HA[("🏠 Home Assistant<br/>Core")]
%% Entry Point
SETUP["__init__.py<br/>async_setup_entry()"]
%% Core Components
API["api.py<br/>TibberPricesApiClient<br/><br/>GraphQL queries"]
COORD["coordinator.py<br/>TibberPricesDataUpdateCoordinator<br/><br/>Orchestrates updates every 15min"]
%% Caching Layers
CACHE_API["💾 API Cache<br/>coordinator/cache.py<br/><br/>HA Storage (persistent)<br/>User: 24h | Prices: until midnight"]
CACHE_TRANS["💾 Transformation Cache<br/>coordinator/data_transformation.py<br/><br/>Memory (enriched prices)<br/>Until config change or midnight"]
CACHE_PERIOD["💾 Period Cache<br/>coordinator/periods.py<br/><br/>Memory (calculated periods)<br/>Hash-based invalidation"]
CACHE_CONFIG["💾 Config Cache<br/>coordinator/*<br/><br/>Memory (parsed options)<br/>Until config change"]
CACHE_TRANS_TEXT["💾 Translation Cache<br/>const.py<br/><br/>Memory (UI strings)<br/>Until HA restart"]
%% Processing Components
TRANSFORM["coordinator/data_transformation.py<br/>DataTransformer<br/><br/>Enrich prices with statistics"]
PERIODS["coordinator/periods.py<br/>PeriodCalculator<br/><br/>Calculate best/peak periods"]
ENRICH["price_utils.py + average_utils.py<br/><br/>Calculate trailing/leading averages<br/>rating_level, differences"]
%% Output Components
SENSORS["sensor/<br/>TibberPricesSensor<br/><br/>120+ price/level/rating sensors"]
BINARY["binary_sensor/<br/>TibberPricesBinarySensor<br/><br/>Period indicators"]
SERVICES["services/<br/><br/>Custom service endpoints<br/>(get_chartdata, ApexCharts)"]
%% Flow Connections
TIBBER -->|"Query user data<br/>Query prices<br/>(yesterday/today/tomorrow)"| API
API -->|"Raw GraphQL response"| COORD
COORD -->|"Check cache first"| CACHE_API
CACHE_API -.->|"Cache hit:<br/>Return cached"| COORD
CACHE_API -.->|"Cache miss:<br/>Fetch from API"| API
COORD -->|"Raw price data"| TRANSFORM
TRANSFORM -->|"Check cache"| CACHE_TRANS
CACHE_TRANS -.->|"Cache hit"| TRANSFORM
CACHE_TRANS -.->|"Cache miss"| ENRICH
ENRICH -->|"Enriched data"| TRANSFORM
TRANSFORM -->|"Enriched price data"| COORD
COORD -->|"Enriched data"| PERIODS
PERIODS -->|"Check cache"| CACHE_PERIOD
CACHE_PERIOD -.->|"Hash match:<br/>Return cached"| PERIODS
CACHE_PERIOD -.->|"Hash mismatch:<br/>Recalculate"| PERIODS
PERIODS -->|"Calculated periods"| COORD
COORD -->|"Complete data<br/>(prices + periods)"| SENSORS
COORD -->|"Complete data"| BINARY
COORD -->|"Data access"| SERVICES
SENSORS -->|"Entity states"| HA
BINARY -->|"Entity states"| HA
SERVICES -->|"Service responses"| HA
%% Config access
CACHE_CONFIG -.->|"Parsed options"| TRANSFORM
CACHE_CONFIG -.->|"Parsed options"| PERIODS
CACHE_TRANS_TEXT -.->|"UI strings"| SENSORS
CACHE_TRANS_TEXT -.->|"UI strings"| BINARY
SETUP -->|"Initialize"| COORD
SETUP -->|"Register"| SENSORS
SETUP -->|"Register"| BINARY
SETUP -->|"Register"| SERVICES
%% Styling
classDef external fill:#e1f5ff,stroke:#0288d1,stroke-width:3px
classDef cache fill:#fff3e0,stroke:#f57c00,stroke-width:2px
classDef processing fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef output fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
class TIBBER,HA external
class CACHE_API,CACHE_TRANS,CACHE_PERIOD,CACHE_CONFIG,CACHE_TRANS_TEXT cache
class TRANSFORM,PERIODS,ENRICH processing
class SENSORS,BINARY,SERVICES output
```
### Flow Description
1. **Setup** (`__init__.py`)
- 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)
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
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
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)
6. **Service Calls**
- Custom services access coordinator data directly
- Return formatted responses (JSON, ApexCharts format)
---
## Caching Architecture
### Overview
The integration uses **5 independent caching layers** for optimal performance:
| Layer | Location | Lifetime | Invalidation | Memory |
|-------|----------|----------|--------------|--------|
| **API Cache** | `coordinator/cache.py` | 24h (user)<br/>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)
### Cache Coordination
```mermaid
flowchart LR
USER[("User changes options")]
MIDNIGHT[("Midnight turnover")]
NEWDATA[("Tomorrow data arrives")]
USER -->|"Explicit invalidation"| CONFIG["Config Cache<br/>❌ Clear"]
USER -->|"Explicit invalidation"| PERIOD["Period Cache<br/>❌ Clear"]
USER -->|"Explicit invalidation"| TRANS["Transformation Cache<br/>❌ Clear"]
MIDNIGHT -->|"Date validation"| API["API Cache<br/>❌ Clear prices"]
MIDNIGHT -->|"Date check"| TRANS
NEWDATA -->|"Hash mismatch"| PERIOD
CONFIG -.->|"Next access"| CONFIG_NEW["Reparse options"]
PERIOD -.->|"Next access"| PERIOD_NEW["Recalculate"]
TRANS -.->|"Next access"| TRANS_NEW["Re-enrich"]
API -.->|"Next access"| API_NEW["Fetch from API"]
classDef invalid fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef rebuild fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
class CONFIG,PERIOD,TRANS,API invalid
class CONFIG_NEW,PERIOD_NEW,TRANS_NEW,API_NEW rebuild
```
**Key insight:** No cascading invalidations - each cache is independent and rebuilds on-demand.
For detailed cache behavior, see [Caching Strategy](./caching-strategy.md).
---
## Component Responsibilities
### 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) |
### 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 |
**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
- `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
- `timing.py` - Best/peak price period timing
- `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
- Easy to add sensors: Choose calculation pattern, add to routing
### 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 |
---
## Key Patterns
### 1. Dual Translation System
- **Standard translations** (`/translations/*.json`): HA-compliant schema for entity names
- **Custom translations** (`/custom_translations/*.json`): Extended descriptions, usage tips
- Both loaded at integration setup, cached in memory
- Access via `get_translation()` helper function
### 2. Price Data Enrichment
All quarter-hourly price intervals get augmented via `utils/price.py`:
```python
# Original from Tibber API
{
"startsAt": "2025-11-03T14:00:00+01:00",
"total": 0.2534,
"level": "NORMAL"
}
# After enrichment (utils/price.py)
{
"startsAt": "2025-11-03T14:00:00+01:00",
"total": 0.2534,
"level": "NORMAL",
"trailing_avg_24h": 0.2312, # ← Added: 24h trailing average
"difference": 9.6, # ← Added: % diff from trailing avg
"rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```
### 3. Quarter-Hour Precision
- **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)
- **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)
- **Result**: Current price sensors update without waiting for next API poll
### 4. Calculator Pattern (Sensor Platform)
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
- Independent testability for each component
---
## Performance Characteristics
### API Call Reduction
- **Without caching:** 96 API calls/day (every 15 min)
- **With caching:** ~1-2 API calls/day (only when cache expires)
- **Reduction:** ~98%
### 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 |
### Memory Usage
- **Per coordinator instance:** ~126KB cache overhead
- **Typical setup:** 1 main + 2 subentries = ~378KB total
- **Redundancy eliminated:** 14% reduction (10KB saved per coordinator)
---
## Related Documentation
- **[Timer Architecture](./timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers)
- **[Caching Strategy](./caching-strategy.md)** - Detailed cache behavior, invalidation, debugging
- **[Setup Guide](./setup.md)** - Development environment setup
- **[Testing Guide](./testing.md)** - How to test changes
- **[Release Management](./release-management.md)** - Release workflow and versioning
- **[AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md)** - Complete reference for AI development

View file

@ -0,0 +1,447 @@
---
comments: false
---
# Caching Strategy
This document explains all caching mechanisms in the Tibber Prices integration, their purpose, invalidation logic, and lifetime.
For timer coordination and scheduling details, see [Timer Architecture](./timer-architecture.md).
## Overview
The integration uses **4 distinct caching layers** with different purposes and lifetimes:
1. **Persistent API Data Cache** (HA Storage) - Hours to days
2. **Translation Cache** (Memory) - Forever (until HA restart)
3. **Config Dictionary Cache** (Memory) - Until config changes
4. **Period Calculation Cache** (Memory) - Until price data or config changes
## 1. Persistent API Data Cache
**Location:** `coordinator/cache.py` → HA Storage (`.storage/tibber_prices.<entry_id>`)
**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
**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()
```
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
```
3. **Tomorrow data check** (after 13:00):
```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.
---
## 2. Translation Cache
**Location:** `const.py``_TRANSLATIONS_CACHE` and `_STANDARD_TRANSLATIONS_CACHE` (in-memory dicts)
**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")
```
**Why this cache matters:** Entity attributes are accessed on every state update (~15 times per hour per entity). File I/O would block the event loop. Cache enables synchronous, non-blocking attribute generation.
---
## 3. Config Dictionary Cache
**Location:** `coordinator/data_transformation.py` and `coordinator/periods.py` (per-instance fields)
**Purpose:** Avoid ~30-40 `options.get()` calls on every coordinator update (every 15 minutes).
**What is cached:**
### DataTransformer Config Cache
```python
{
"thresholds": {"low": 15, "high": 35},
"volatility_thresholds": {"moderate": 15.0, "high": 25.0, "very_high": 40.0},
# ... 20+ more config fields
}
```
### PeriodCalculator Config Cache
```python
{
"best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60},
"peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}
}
```
**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()
```
**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)
**Why this cache matters:** Config is read multiple times per update (transformation + period calculation + validation). Caching eliminates redundant lookups without changing behavior.
---
## 4. Period Calculation Cache
**Location:** `coordinator/periods.py``PeriodCalculator._cached_periods`
**Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed.
**What is cached:**
```python
{
"best_price": {
"periods": [...], # Calculated period objects
"intervals": [...], # All intervals in periods
"metadata": {...} # Config snapshot
},
"best_price_relaxation": {"relaxation_active": bool, ...},
"peak_price": {...},
"peak_price_relaxation": {...}
}
```
**Cache key:** Hash of relevant inputs
```python
hash_data = (
today_signature, # (startsAt, rating_level) for each interval
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
)
```
**Lifetime:**
- Until price data changes (today's intervals modified)
- Until config changes (flex, thresholds, filters)
- Recalculated at midnight (new today data)
**Invalidation triggers:**
1. **Config change** (explicit):
```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
```
**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)
**Why this cache matters:** Period calculation is CPU-intensive (filtering, gap tolerance, relaxation). Caching avoids recalculating unchanged periods 3-4 times per hour.
---
## 5. Transformation Cache (Price Enrichment Only)
**Location:** `coordinator/data_transformation.py``_cached_transformed_data`
**Status:** ✅ **Clean separation** - enrichment only, no redundancy
**What is cached:**
```python
{
"timestamp": ...,
"homes": {...},
"priceInfo": {...}, # Enriched price data (trailing_avg_24h, difference, rating_level)
# NO periods - periods are exclusively managed by PeriodCalculator
}
```
**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
**Architecture:**
- DataTransformer: Handles price enrichment only
- PeriodCalculator: Handles period calculation only (with hash-based cache)
- Coordinator: Assembles final data on-demand from both caches
**Memory savings:** Eliminating redundant period storage saves ~10KB per coordinator (14% reduction).
---
## Cache Invalidation Flow
### User Changes Options (Config Flow)
```
User saves options
config_entry.add_update_listener() triggers
coordinator._handle_options_update()
├─> DataTransformer.invalidate_config_cache()
│ └─> _config_cache = None
│ _config_cache_valid = False
│ _cached_transformed_data = None
└─> PeriodCalculator.invalidate_config_cache()
└─> _config_cache = None
_config_cache_valid = False
_cached_periods = None
_last_periods_hash = None
coordinator.async_request_refresh()
Fresh data fetch with new config
```
### Midnight Turnover (Day Transition)
```
Timer #2 fires at 00:00
coordinator._handle_midnight_turnover()
├─> Clear persistent cache
│ └─> _cached_price_data = None
│ _last_price_update = None
└─> Clear transformation cache
└─> _cached_transformed_data = None
_last_transformation_config = None
Period cache auto-invalidates (hash mismatch on new "today")
Fresh API fetch for new day
```
### Tomorrow Data Arrives (~13:00)
```
Coordinator update cycle
should_update_price_data() checks tomorrow
Tomorrow data missing/invalid
API fetch with new tomorrow data
Price data hash changes (new intervals)
Period cache auto-invalidates (hash mismatch)
Periods recalculated with tomorrow included
```
---
## Cache Coordination
**All caches work together:**
```
Persistent Storage (HA restart)
API Data Cache (price_data, user_data)
├─> Enrichment (add rating_level, difference, etc.)
│ ↓
│ Transformation Cache (_cached_transformed_data)
└─> Period Calculation
Period Cache (_cached_periods)
Config Cache (avoid re-reading options)
Translation Cache (entity descriptions)
```
**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)
---
## Performance Characteristics
### Typical Operation (No Changes)
```
Coordinator Update (every 15 min)
├─> API fetch: SKIP (cache valid)
├─> Config dict build: ~1μs (cached)
├─> Period calculation: ~1ms (cached, hash match)
├─> Transformation: ~10ms (enrichment only, periods cached)
└─> Entity updates: ~5ms (translation cache hit)
Total: ~16ms (down from ~600ms without caching)
```
### After Midnight Turnover
```
Coordinator Update (00:00)
├─> API fetch: ~500ms (cache cleared, fetch new day)
├─> Config dict build: ~50μs (rebuild, no cache)
├─> Period calculation: ~200ms (cache miss, recalculate)
├─> Transformation: ~50ms (re-enrich, rebuild)
└─> Entity updates: ~5ms (translation cache still valid)
Total: ~755ms (expected once per day)
```
### After Config Change
```
Options Update
├─> Cache invalidation: `<`1ms
├─> Coordinator refresh: ~600ms
│ ├─> API fetch: SKIP (data unchanged)
│ ├─> Config rebuild: ~50μs
│ ├─> Period recalculation: ~200ms (new thresholds)
│ ├─> Re-enrichment: ~50ms
│ └─> Entity updates: ~5ms
└─> Total: ~600ms (expected on manual reconfiguration)
```
---
## 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 |
**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
---
## 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?
**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
**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
**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
**Fix:** Re-install integration or restart HA to reload translation files.
---
## Related Documentation
- **[Timer Architecture](./timer-architecture.md)** - Timer system, scheduling, midnight coordination
- **[Architecture](./architecture.md)** - Overall system design, data flow
- **[AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md)** - Complete reference for AI development

View file

@ -0,0 +1,121 @@
---
comments: false
---
# Coding Guidelines
> **Note:** For complete coding standards, see [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md).
## Code Style
- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort)
- **Max line length**: 120 characters
- **Max complexity**: 25 (McCabe)
- **Target**: Python 3.13
Run before committing:
```bash
./scripts/lint # Auto-fix issues
./scripts/release/hassfest # Validate integration structure
```
## Naming Conventions
### Class Names
**All public classes MUST use the integration name as prefix.**
This is a Home Assistant standard to avoid naming conflicts between integrations.
```python
# ✅ CORRECT
class TibberPricesApiClient:
class TibberPricesDataUpdateCoordinator:
class TibberPricesSensor:
# ❌ WRONG - Missing prefix
class ApiClient:
class DataFetcher:
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
**Private Classes:**
If a helper class is ONLY used within a single module file, prefix it with underscore:
```python
# ✅ Private class - used only in this file
class _InternalHelper:
"""Helper used only within this module."""
pass
# ❌ Wrong - no prefix but used across modules
class DataFetcher: # Should be TibberPricesDataFetcher
pass
```
**Note:** Currently (Nov 2025), this project has **NO private classes** - all classes are used across module boundaries.
**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
See [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md) for complete list of classes needing rename.
## Import Order
1. Python stdlib (specific types only)
2. Third-party (`homeassistant.*`, `aiohttp`)
3. Local (`.api`, `.const`)
## Critical Patterns
### Time Handling
Always use `dt_util` from `homeassistant.util`:
```python
from homeassistant.util import dt as dt_util
price_time = dt_util.parse_datetime(starts_at)
price_time = dt_util.as_local(price_time) # Convert to HA timezone
now = dt_util.now()
```
### Translation Loading
```python
# In __init__.py async_setup_entry:
await async_load_translations(hass, "en")
await async_load_standard_translations(hass, "en")
```
### Price Data Enrichment
Always enrich raw API data:
```python
from .price_utils import enrich_price_info_with_differences
enriched = enrich_price_info_with_differences(
price_info_data,
thresholds,
)
```
See [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md) for complete guidelines.

View file

@ -0,0 +1,216 @@
# Contributing Guide
Welcome! This guide helps you contribute to the Tibber Prices integration.
## Getting Started
### Prerequisites
- Git
- VS Code with Remote Containers extension
- Docker Desktop
### Fork and Clone
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
```
3. Open in VS Code
4. Click "Reopen in Container" when prompted
The DevContainer will set up everything automatically.
## Development Workflow
### 1. Create a Branch
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/issue-123-description
```
**Branch naming:**
- `feature/` - New features
- `fix/` - Bug fixes
- `docs/` - Documentation only
- `refactor/` - Code restructuring
- `test/` - Test improvements
### 2. Make Changes
Edit code, following [Coding Guidelines](coding-guidelines.md).
**Run checks frequently:**
```bash
./scripts/type-check # Pyright type checking
./scripts/lint # Ruff linting (auto-fix)
./scripts/test # Run tests
```
### 3. Test Locally
```bash
./scripts/develop # Start HA with integration loaded
```
Access at http://localhost:8123
### 4. Write Tests
Add tests in `/tests/` for new features:
```python
@pytest.mark.unit
async def test_your_feature(hass, coordinator):
"""Test your new feature."""
# Arrange
coordinator.data = {...}
# Act
result = your_function(coordinator.data)
# Assert
assert result == expected_value
```
Run your test:
```bash
./scripts/test tests/test_your_feature.py -v
```
### 5. Commit Changes
Follow [Conventional Commits](https://www.conventionalcommits.org/):
```bash
git add .
git commit -m "feat(sensors): add volatility trend sensor
Add new sensor showing 3-hour volatility trend direction.
Includes attributes with historical volatility data.
Impact: Users can predict when prices will stabilize or continue fluctuating."
```
**Commit types:**
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation
- `refactor:` - Code restructuring
- `test:` - Test changes
- `chore:` - Maintenance
**Add scope when relevant:**
- `feat(sensors):` - Sensor platform
- `fix(coordinator):` - Data coordinator
- `docs(user):` - User documentation
### 6. Push and Create PR
```bash
git push origin your-branch-name
```
Then open Pull Request on GitHub.
## Pull Request Guidelines
### PR Template
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`)
- [ ] Linting passes (`./scripts/lint-check`)
- [ ] Documentation updated (if needed)
- [ ] AGENTS.md updated (if patterns changed)
- [ ] Commit messages follow Conventional Commits
### Review Process
1. **Automated checks** run (CI/CD)
2. **Maintainer review** (usually within 3 days)
3. **Address feedback** if requested
4. **Approval** → Maintainer merges
## Code Review Tips
### What Reviewers Look For
✅ **Good:**
- Clear, self-explanatory code
- Appropriate comments for complex logic
- Tests covering edge cases
- Type hints on all functions
- Follows existing patterns
❌ **Avoid:**
- Large PRs (>500 lines) - split into smaller ones
- Mixing unrelated changes
- Missing tests for new features
- Breaking changes without migration path
- Copy-pasted code (refactor into shared functions)
### Responding to Feedback
- Don't take it personally - we're improving code together
- Ask questions if feedback unclear
- Push additional commits to address comments
- Mark conversations as resolved when fixed
## Finding Issues to Work On
Good first issues are labeled:
- `good first issue` - Beginner-friendly
- `help wanted` - Maintainers welcome contributions
- `documentation` - Docs improvements
Comment on issue before starting work to avoid duplicates.
## Communication
- **GitHub Issues** - Bug reports, feature requests
- **Pull Requests** - Code discussion
- **Discussions** - General questions, ideas
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
- [Release Management](release-management.md) - How releases work

View file

@ -0,0 +1,286 @@
---
comments: false
---
# Critical Behavior Patterns - Testing Guide
**Purpose:** This documentation lists essential behavior patterns that must be tested to ensure production-quality code and prevent resource leaks.
**Last Updated:** 2025-11-22
**Test Coverage:** 41 tests implemented (100% of critical patterns)
## 🎯 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
- **File Handle Leaks**: Storage files remain open → system resources exhausted
## ✅ Test Categories
### 1. Resource Cleanup (Memory Leak Prevention)
**File:** `tests/test_resource_cleanup.py`
#### 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()`)
- Sensor cleanup removes ALL registered listeners
- 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()`
- `binary_sensor/core.py``async_will_remove_from_hass()`
#### 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()`
### 2. Cache Invalidation ✅
**File:** `tests/test_resource_cleanup.py`
#### 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()`
### 3. Storage Cleanup ✅
**File:** `tests/test_resource_cleanup.py` + `tests/test_coordinator_shutdown.py`
#### 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()`
### 4. Timer Scheduling ✅
**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
### 5. Sensor-to-Timer Assignment ✅
**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
## 🚨 Additional Analysis (Nice-to-Have Patterns)
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
**If needed:** `_chart_refresh_task` tracking + cancel in `async_will_remove_from_hass()`
### 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
**Code:** `api/client.py` + `__init__.py`
### 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
**Code:** `const.py``_TRANSLATIONS_CACHE`, `_STANDARD_TRANSLATIONS_CACHE`
### 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": {...},
"priceInfo": [...], # Flat list of all enriched intervals
"currency": "EUR" # Top-level for easy access
}
```
### 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
**Code:** `services/apexcharts.py`
## 📊 Test Coverage Status
### ✅ 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)** |
### 📋 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 |
**Legend:**
- ✅ = Fully tested or pattern verified correct
- 📋 = Analyzed, low priority for testing (no known issues)
## 🎯 Development Status
### ✅ 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)
- ✅ Caches are invalidated (no stale data issues)
- ✅ Storage is saved and cleaned up (no data loss)
- ✅ Timer scheduling works correctly (no update issues)
- ✅ Sensor-timer assignment is correct (no wrong updates)
### 📋 Nice-to-Have Tests (Optional)
If problems arise in the future, these tests can be added:
1. **Async Task Management** - Pattern analyzed (fire-and-forget for short tasks)
2. **Data Structure Integrity** - Midnight rotation manually tested
3. **Service Response Memory** - HA's response lifecycle automatic
**Conclusion:** The integration has production-quality test coverage for all critical resource leak patterns.
## 🔍 How to Run Tests
```bash
# Run all resource cleanup tests (14 tests)
./scripts/test tests/test_resource_cleanup.py -v
# Run all critical pattern tests (41 tests)
./scripts/test tests/test_resource_cleanup.py tests/test_coordinator_shutdown.py \
tests/test_timer_scheduling.py tests/test_sensor_timer_assignment.py -v
# Run all tests with coverage
./scripts/test --cov=custom_components.tibber_prices --cov-report=html
# Type checking and linting
./scripts/check
# Manual memory leak test
# 1. Start HA: ./scripts/develop
# 2. Monitor RAM: watch -n 1 'ps aux | grep home-assistant'
# 3. Reload integration multiple times (HA UI: Settings → Devices → Tibber Prices → Reload)
# 4. RAM should stabilize (not grow continuously)
```
## 📚 References
- **Home Assistant Cleanup Patterns**: https://developers.home-assistant.io/docs/integration_setup_failures/#cleanup
- **Async Best Practices**: https://developers.home-assistant.io/docs/asyncio_101/
- **Memory Profiling**: https://docs.python.org/3/library/tracemalloc.html

View file

@ -0,0 +1,230 @@
# Debugging Guide
Tips and techniques for debugging the Tibber Prices integration during development.
## Logging
### Enable Debug Logging
Add to `configuration.yaml`:
```yaml
logger:
default: info
logs:
custom_components.tibber_prices: debug
```
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
[custom_components.tibber_prices.coordinator] Midnight turnover detected, clearing cache
```
**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
[custom_components.tibber_prices.coordinator.periods] Period 1: 02:00-05:00 (12 intervals)
```
**API Errors:**
```
[custom_components.tibber_prices.api] API request failed: Unauthorized
[custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s
```
## VS Code Debugging
### Launch Configuration
`.vscode/launch.json`:
```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"
}
}
]
}
```
### Set Breakpoints
**Coordinator update:**
```python
# coordinator/core.py
async def _async_update_data(self) -> dict:
"""Fetch data from API."""
breakpoint() # Or set VS Code breakpoint
```
**Period calculation:**
```python
# coordinator/period_handlers/core.py
def calculate_periods(...) -> list[dict]:
"""Calculate best/peak price periods."""
breakpoint()
```
## pytest Debugging
### Run Single Test with Output
```bash
.venv/bin/python -m pytest tests/test_period_calculation.py::test_midnight_crossing -v -s
```
**Flags:**
- `-v` - Verbose output
- `-s` - Show print statements
- `-k pattern` - Run tests matching pattern
### Debug Test in VS Code
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}")
print(f"Price info count: {len(coordinator.data['priceInfo'])}")
```
**Inspect period attributes:**
```python
def test_periods(hass, coordinator):
periods = coordinator.data.get('best_price_periods', [])
for period in periods:
print(f"Period: {period['start']} to {period['end']}")
print(f" Intervals: {len(period['intervals'])}")
```
## Common Issues
### 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/*`
### 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",
self.entity_id, self._attr_native_value, new_value)
```
### Period Calculation Wrong
**Enable detailed period logs:**
```python
# coordinator/period_handlers/period_building.py
_LOGGER.debug("Candidate intervals: %s",
[(i['startsAt'], i['total']) for i in candidates])
```
**Check filter statistics:**
```
[period_building] Flex filter blocked: 45 intervals
[period_building] Min distance blocked: 12 intervals
[period_building] Level filter blocked: 8 intervals
```
## Performance Profiling
### Time Execution
```python
import time
start = time.perf_counter()
result = expensive_function()
duration = time.perf_counter() - start
_LOGGER.debug("Function took %.3fs", duration)
```
### Memory Usage
```python
import tracemalloc
tracemalloc.start()
# ... your code ...
current, peak = tracemalloc.get_traced_memory()
_LOGGER.debug("Memory: current=%d peak=%d", current, peak)
tracemalloc.stop()
```
### Profile with cProfile
```bash
python -m cProfile -o profile.stats -m homeassistant -c config
python -m pstats profile.stats
# Then: sort cumtime, stats 20
```
## Live Debugging in Running HA
### Remote Debugging with debugpy
Add to coordinator code:
```python
import debugpy
debugpy.listen(5678)
_LOGGER.info("Waiting for debugger attach on port 5678")
debugpy.wait_for_client()
```
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
```
---
💡 **Related:**
- [Testing Guide](testing.md) - Writing and running tests
- [Setup Guide](setup.md) - Development environment
- [Architecture](architecture.md) - Code structure

View file

@ -0,0 +1,185 @@
# Developer Documentation
This section contains documentation for contributors and maintainers of the **Tibber Prices custom integration**.
:::info Community Project
This is an independent, community-maintained custom integration for Home Assistant. It is **not** an official Tibber product and is **not** affiliated with Tibber AS.
:::
## 📚 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
## 🤖 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
**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.
### AI-Assisted Development
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
**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
**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
**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
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.
## 🚀 Quick Start for Contributors
1. **Fork and clone** the repository
2. **Open in DevContainer** (VS Code: "Reopen in Container")
3. **Run setup**: `./scripts/setup/setup` (happens automatically via `postCreateCommand`)
4. **Start development environment**: `./scripts/develop`
5. **Make your changes** following the [Coding Guidelines](coding-guidelines.md)
6. **Run linting**: `./scripts/lint`
7. **Validate integration**: `./scripts/release/hassfest`
8. **Test your changes** in the running Home Assistant instance
9. **Commit using Conventional Commits** format
10. **Open a Pull Request** with clear description
## 🛠️ Development Tools
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
## 📦 Project Structure
```
custom_components/tibber_prices/
├── __init__.py # Integration setup
├── coordinator.py # Data update coordinator with caching
├── api.py # Tibber GraphQL API client
├── price_utils.py # Price enrichment functions
├── average_utils.py # Average calculation utilities
├── sensor/ # Sensor platform (package)
│ ├── __init__.py # Platform setup
│ ├── core.py # TibberPricesSensor class
│ ├── definitions.py # Entity descriptions
│ ├── helpers.py # Pure helper functions
│ └── attributes.py # Attribute builders
├── binary_sensor.py # Binary sensor platform
├── entity_utils/ # Shared entity helpers
│ ├── icons.py # Icon mapping logic
│ ├── colors.py # Color mapping logic
│ └── attributes.py # Common attribute builders
├── services.py # Custom services
├── config_flow.py # UI configuration flow
├── const.py # Constants and helpers
├── translations/ # Standard HA translations
└── custom_translations/ # Extended translations (descriptions)
```
## 🔍 Key Concepts
**DataUpdateCoordinator Pattern:**
- 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`
**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
## 🧪 Testing
```bash
# Validate integration structure
./scripts/release/hassfest
# Run all tests
pytest tests/
# Run specific test file
pytest tests/test_coordinator.py
# Run with coverage
pytest --cov=custom_components.tibber_prices tests/
```
## 📝 Documentation Standards
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)
**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
## 🤝 Contributing
See [CONTRIBUTING.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/CONTRIBUTING.md) for detailed contribution guidelines, code of conduct, and pull request process.
## 📄 License
This project is licensed under the [MIT License](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/LICENSE).
---
**Note:** This documentation is for developers. End users should refer to the [User Documentation](https://jpawlowski.github.io/hass.tibber_prices/user/).

View file

@ -0,0 +1,322 @@
# Performance Optimization
Guidelines for maintaining and improving integration performance.
## Performance Goals
Target metrics:
- **Coordinator update**: &lt;500ms (typical: 200-300ms)
- **Sensor update**: &lt;10ms per sensor
- **Period calculation**: &lt;100ms (typical: 20-50ms)
- **Memory footprint**: &lt;10MB per home
- **API calls**: &lt;100 per day per home
## Profiling
### Timing Decorator
Use for performance-critical functions:
```python
import time
import functools
def timing(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
duration = time.perf_counter() - start
_LOGGER.debug("%s took %.3fms", func.__name__, duration * 1000)
return result
return wrapper
@timing
def expensive_calculation():
# Your code here
```
### Memory Profiling
```python
import tracemalloc
tracemalloc.start()
# Run your code
current, peak = tracemalloc.get_traced_memory()
_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB",
current / 1024**2, peak / 1024**2)
tracemalloc.stop()
```
### Async Profiling
```bash
# Install aioprof
uv pip install aioprof
# Run with profiling
python -m aioprof homeassistant -c config
```
## Optimization Patterns
### Caching
**1. Persistent Cache** (API data):
```python
# Already implemented in coordinator/cache.py
store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
data = await store.async_load()
```
**2. Translation Cache** (in-memory):
```python
# Already implemented in const.py
_TRANSLATION_CACHE: dict[str, dict] = {}
def get_translation(path: str, language: str) -> dict:
cache_key = f"{path}_{language}"
if cache_key not in _TRANSLATION_CACHE:
_TRANSLATION_CACHE[cache_key] = load_translation(path, language)
return _TRANSLATION_CACHE[cache_key]
```
**3. Config Cache** (invalidated on options change):
```python
class DataTransformer:
def __init__(self):
self._config_cache: dict | None = None
def get_config(self) -> dict:
if self._config_cache is None:
self._config_cache = self._build_config()
return self._config_cache
def invalidate_config_cache(self):
self._config_cache = None
```
### Lazy Loading
**Load data only when needed:**
```python
@property
def extra_state_attributes(self) -> dict | None:
"""Return attributes."""
# Calculate only when accessed
if self.entity_description.key == "complex_sensor":
return self._calculate_complex_attributes()
return None
```
### Bulk Operations
**Process multiple items at once:**
```python
# ❌ Slow - loop with individual operations
for interval in intervals:
enriched = enrich_single_interval(interval)
results.append(enriched)
# ✅ Fast - bulk processing
results = enrich_intervals_bulk(intervals)
```
### Async Best Practices
**1. Concurrent API calls:**
```python
# ❌ Sequential (slow)
user_data = await fetch_user_data()
price_data = await fetch_price_data()
# ✅ Concurrent (fast)
user_data, price_data = await asyncio.gather(
fetch_user_data(),
fetch_price_data()
)
```
**2. Don't block event loop:**
```python
# ❌ Blocking
result = heavy_computation() # Blocks for seconds
# ✅ Non-blocking
result = await hass.async_add_executor_job(heavy_computation)
```
## Memory Management
### Avoid Memory Leaks
**1. Clear references:**
```python
class Coordinator:
async def async_shutdown(self):
"""Clean up resources."""
self._listeners.clear()
self._data = None
self._cache = None
```
**2. Use weak references for callbacks:**
```python
import weakref
class Manager:
def __init__(self):
self._callbacks: list[weakref.ref] = []
def register(self, callback):
self._callbacks.append(weakref.ref(callback))
```
### Efficient Data Structures
**Use appropriate types:**
```python
# ❌ List for lookups (O(n))
if timestamp in timestamp_list:
...
# ✅ Set for lookups (O(1))
if timestamp in timestamp_set:
...
# ❌ List comprehension with filter
results = [x for x in items if condition(x)]
# ✅ Generator for large datasets
results = (x for x in items if condition(x))
```
## Coordinator Optimization
### 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)
```
### Smart Updates
**Only update when needed:**
```python
async def _async_update_data(self) -> dict:
"""Fetch data from API."""
if self._is_cache_valid():
_LOGGER.debug("Using cached data")
return self.data
# Fetch new data
return await self._fetch_data()
```
## Database Impact
### State Class Selection
**Affects long-term statistics storage:**
```python
# ❌ MEASUREMENT for prices (stores every change)
state_class=SensorStateClass.MEASUREMENT # ~35K records/year
# ✅ None for prices (no long-term stats)
state_class=None # Only current state
# ✅ TOTAL for counters only
state_class=SensorStateClass.TOTAL # For cumulative values
```
### Attribute Size
**Keep attributes minimal:**
```python
# ❌ Large nested structures (KB per update)
attributes = {
"all_intervals": [...], # 384 intervals
"full_history": [...], # Days of data
}
# ✅ Essential data only (bytes per update)
attributes = {
"timestamp": "...",
"rating_level": "...",
"next_interval": "...",
}
```
## Testing Performance
### Benchmark Tests
```python
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""
start = time.perf_counter()
periods = calculate_periods(coordinator.data)
duration = time.perf_counter() - start
assert duration < 0.1, f"Too slow: {duration:.3f}s"
```
### Load Testing
```python
@pytest.mark.integration
async def test_multiple_homes_performance(hass):
"""Test with 10 homes."""
coordinators = []
for i in range(10):
coordinator = create_coordinator(hass, home_id=f"home_{i}")
await coordinator.async_refresh()
coordinators.append(coordinator)
# Verify memory usage
# Verify update times
```
## Monitoring in Production
### Log Performance Metrics
```python
@timing
async def _async_update_data(self) -> dict:
"""Fetch data with timing."""
result = await self._fetch_data()
_LOGGER.info("Update completed in %.2fs", timing_duration)
return result
```
### Memory Tracking
```python
import psutil
import os
process = psutil.Process(os.getpid())
memory_mb = process.memory_info().rss / 1024**2
_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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,357 @@
# Recorder History Optimization
**Status**: ✅ IMPLEMENTED
**Last Updated**: 2025-12-07
## Overview
This document describes the implementation of `_unrecorded_attributes` for Tibber Prices entities to prevent Home Assistant Recorder database bloat by excluding non-essential attributes from historical data storage.
**Reference**: [HA Developer Docs - Excluding State Attributes](https://developers.home-assistant.io/docs/core/entity/#excluding-state-attributes-from-recorder-history)
## Implementation
Both `TibberPricesSensor` and `TibberPricesBinarySensor` implement `_unrecorded_attributes` as a class-level `frozenset` to exclude attributes that don't provide value in historical data analysis.
### Pattern
```python
class TibberPricesSensor(TibberPricesEntity, SensorEntity):
"""tibber_prices Sensor class."""
_unrecorded_attributes = frozenset(
{
"description",
"usage_tips",
# ... more attributes
}
)
```
**Key Points:**
- Must be a **class attribute** (not instance attribute)
- Use `frozenset` for immutability and performance
- Applied automatically by Home Assistant's Recorder component
## Categories of Excluded Attributes
### 1. Descriptions/Help Text
**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
- Can be retrieved from translation files when needed
**Impact:** ~500-1000 bytes saved per state change
### 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
- `current_trend_attributes` - Current trend details
- `trend_change_attributes` - Trend change analysis
- `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
- Provide limited value in historical analysis (current state usually sufficient)
**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
}
]
}
```
### 3. Frequently Changing Diagnostics
**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
- Clutter history with cosmetic changes
- Can be reconstructed from other attributes if needed
**Impact:** Prevents unnecessary state writes when only cosmetic attributes change
**Example:** `icon_color` changes from `#00ff00` to `#ffff00` but price hasn't changed → No state write needed
### 4. Static/Rarely Changing Configuration
**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
**Impact:** ~100-200 bytes saved per state change
### 5. Temporary/Time-Bound Data
**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
**Note:** The entity's `native_value` (the actual price/state) is always recorded by HA as the entity state itself — independently of `_unrecorded_attributes`. So excluding `timestamp` does not create a gap in the time-series; the state row already carries the recording timestamp.
**Impact:** ~200-400 bytes saved per state change
**Example:** `next_api_poll: "2025-12-07T14:30:00"` stored at 14:15 is useless when viewing history at 15:00
### 6. Relaxation Details
**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
**Impact:** ~50-100 bytes saved per state change
### 7. Redundant/Derived Data
**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
**Impact:** ~100-200 bytes saved per state change
**Example:** `price_spread = price_max - price_min` (both are recorded, so spread can be calculated)
## Attributes That ARE Recorded
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
### Space Savings
**Per state change:**
- Before: ~3-8 KB average
- After: ~0.5-1.5 KB average
- **Reduction: 60-85%**
**Daily per sensor:**
| Sensor Type | Updates/Day | Before | After | Savings |
|------------|-------------|--------|-------|---------|
| High-frequency (15min) | 96 | ~290 KB | ~140 KB | 50% |
| Low-frequency (6h) | 4 | ~32 KB | ~6 KB | 80% |
### Most Impactful Exclusions
1. **`periods` array** (binary_sensor) - Saves 2-5 KB per state
2. **`data`** (chart_data_export) - Saves 5-20 KB per state
3. **`trend_attributes`** - Saves 1-2 KB per state
4. **`description`/`usage_tips`** - Saves 500-1000 bytes per state
5. **`icon_color`** - Prevents unnecessary state changes
### Real-World Impact
For a typical installation with:
- 80+ sensors
- Updates every 15 minutes
- ~10 sensors updating every minute
**Before:** ~1.5 GB per month
**After:** ~400-500 MB per month
**Savings:** ~1 GB per month (~66% reduction)
## Implementation Files
- **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py`
- Class: `TibberPricesSensor`
- 46 attributes excluded
- **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py`
- Class: `TibberPricesBinarySensor`
- 29 attributes excluded
## When to Update _unrecorded_attributes
### Add to Exclusion List When:
✅ Adding new **description/help text** attributes
✅ Adding **large nested structures** (arrays, complex objects)
✅ Adding **frequently changing diagnostic info** (colors, formatted strings)
✅ Adding **temporary/time-bound data** (timestamps that become stale)
✅ Adding **redundant/derived calculations**
### Keep in History When:
**Core price/timing data** needed for analysis
**Boolean status flags** that show state transitions
**Numeric counters** useful for tracking patterns
**Data that helps understand system behavior** over time
## Decision Framework
When adding a new attribute, ask:
1. **Will this be useful in history queries 1 week from now?**
- No → Exclude
- Yes → Keep
2. **Can this be calculated from other recorded attributes?**
- Yes → Exclude
- No → Keep
3. **Is this primarily for current UI display?**
- Yes → Exclude
- No → Keep
4. **Does this change frequently without indicating state change?**
- Yes → Exclude
- No → Keep
5. **Is this larger than 100 bytes and not essential for analysis?**
- Yes → Exclude
- No → Keep
## Testing
After modifying `_unrecorded_attributes`:
1. **Restart Home Assistant** to apply changes
2. **Check Recorder database size** before/after
3. **Verify essential attributes** still appear in history
4. **Confirm excluded attributes** don't appear in new state writes
**SQL Query to check attribute presence:**
```sql
SELECT
state_id,
attributes
FROM states
WHERE entity_id = 'sensor.tibber_home_current_interval_price'
ORDER BY last_updated DESC
LIMIT 5;
```
## Maintenance Notes
- ✅ Must be a **class attribute** (instance attributes are ignored)
- ✅ Use `frozenset` for immutability
- ✅ Only affects **new** state writes (doesn't purge existing history)
- ✅ Attributes still available via `entity.attributes` in templates/automations
- ✅ Only prevents **storage** in Recorder, not runtime availability
## Long-Term Statistics Optimization (`state_class`)
**This is a second, independent mechanism** that controls writes to the HA `statistics` and `statistics_short_term` tables — distinct from `_unrecorded_attributes`, which only affects the `state_attributes` table.
### Why This Matters
- The `state_attributes` table (controlled by `_unrecorded_attributes`) is **auto-purged** after ~10 days
- The `statistics`/`statistics_short_term` tables (controlled by `state_class`) are **never auto-purged** — they grow unbounded
This makes `state_class=TOTAL` on many sensors the primary cause of long-term database bloat for users with long-running installations.
### HA Constraint: MONETARY Device Class
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 | — |
`MEASUREMENT` causes a hassfest validation error for MONETARY sensors, leaving only `TOTAL` or `None`.
### Implementation Decision
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 |
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
- ❌ No writes to `statistics`/`statistics_short_term` tables
### 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)
### Relationship to `_unrecorded_attributes`
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 |
Both optimizations work together. `_unrecorded_attributes` reduces the size of each state write; `state_class=None` eliminates an entire category of unbounded writes.
### Implementation File
- **`custom_components/tibber_prices/sensor/definitions.py`** — `state_class` set per-sensor in `SensorEntityDescription`
## References
- [HA Developer Docs - Excluding State Attributes](https://developers.home-assistant.io/docs/core/entity/#excluding-state-attributes-from-recorder-history)
- Implementation PR: [Link when merged]
- Related Issue: [Link if applicable]

View file

@ -0,0 +1,414 @@
# Refactoring Guide
This guide explains how to plan and execute major refactorings in this project.
## When to Plan a Refactoring
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)
🟡 **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
🟢 **Small changes - no planning needed:**
- Bug fixes (straightforward, `<`100 lines)
- Small features (`<`3 files, clear approach)
- Documentation updates
- Cosmetic changes (formatting, renaming)
## The Planning Process
### 1. Create a Planning Document
Create a file in the `planning/` directory (git-ignored for free iteration):
```bash
# Example:
touch planning/my-feature-refactoring-plan.md
```
**Note:** The `planning/` directory is git-ignored, so you can iterate freely without polluting git history.
### 2. Use the Planning Template
Every planning document should include:
```markdown
# <Feature> Refactoring Plan
**Status**: 🔄 PLANNING | 🚧 IN PROGRESS | ✅ COMPLETED | ❌ CANCELLED
**Created**: YYYY-MM-DD
**Last Updated**: YYYY-MM-DD
## Problem Statement
- What's the issue?
- Why does it need fixing?
- Current pain points
## Proposed Solution
- 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
## Risks & Mitigation
- What could go wrong?
- How to prevent it?
- Rollback strategy
## Success Criteria
- Measurable improvements
- Testing requirements
- Verification steps
```
See `planning/README.md` for detailed template explanation.
### 3. Iterate Freely
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
### 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
### 5. After Completion
**Option A: Archive in docs/development/**
If the plan has lasting value (successful pattern, reusable approach):
```bash
mv planning/my-feature-refactoring-plan.md docs/development/
git add docs/development/my-feature-refactoring-plan.md
git commit -m "docs: archive successful refactoring plan"
```
**Option B: Delete**
If the plan served its purpose and code is the source of truth:
```bash
rm planning/my-feature-refactoring-plan.md
```
**Option C: Keep locally (not committed)**
For "why we didn't do X" reference:
```bash
mkdir -p planning/archive
mv planning/my-feature-refactoring-plan.md planning/archive/
# Still git-ignored, just organized
```
## Real-World Example
The **sensor/ package refactoring** (Nov 2025) is a successful example:
**Before:**
- `sensor.py` - 2,574 lines, hard to navigate
**After:**
- `sensor/` package with 5 focused modules
- Each module `<`800 lines
- Clear separation of concerns
**Process:**
1. Created `planning/module-splitting-plan.md` (now in `docs/development/`)
2. Defined 6 phases with clear file lifecycle
3. Implemented phase by phase
4. Tested after each phase
5. Documented in AGENTS.md
6. Moved plan to `docs/development/` as reference
**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
**Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure.
## Phase-by-Phase Implementation
### Why Phases Matter
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!)
### Phase Structure
Each phase should:
1. **Have clear goal** - What's being changed?
2. **Document file lifecycle** - CREATE/MODIFY/DELETE/RENAME
3. **Define success criteria** - How to verify it worked?
4. **Include testing steps** - What to test?
5. **Estimate time** - Realistic time budget
### Example Phase Documentation
```markdown
### Phase 3: Extract Helper Functions (Session 3)
**Goal**: Move pure utility functions to helpers.py
**File Lifecycle**:
- ✨ CREATE `sensor/helpers.py` (utility functions)
- ✏️ MODIFY `sensor/core.py` (import from helpers.py)
**Steps**:
1. Create sensor/helpers.py
2. Move pure functions (no state, no self)
3. Add comprehensive docstrings
4. Update imports in core.py
**Estimated time**: 45 minutes
**Success criteria**:
- ✅ All pure functions moved
- ✅ `./scripts/lint-check` passes
- ✅ HA starts successfully
- ✅ All entities work correctly
```
## Testing Strategy
### After Each Phase
Minimum testing checklist:
```bash
# 1. Linting passes
./scripts/lint-check
# 2. Home Assistant starts
./scripts/develop
# Watch for startup errors in logs
# 3. Integration loads
# Check: Settings → Devices & Services → Tibber Prices
# Verify: All entities appear
# 4. Basic functionality
# Test: Data updates without errors
# Check: Entity states update correctly
```
### Comprehensive Testing (Final Phase)
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)
## Common Pitfalls
### ❌ Skip Planning for Large Changes
**Problem:** "This seems straightforward, I'll just start coding..."
**Result:** Halfway through, realize the approach doesn't work. Wasted time.
**Solution:** If unsure, spend 30 minutes on a rough plan. Better to plan and discard than get stuck.
### ❌ Implement All Phases at Once
**Problem:** "I'll do all phases, then test everything..."
**Result:** 10+ files changed, 2000+ lines modified, hard to debug if something breaks.
**Solution:** Test after EVERY phase. Commit after each successful phase.
### ❌ Forget to Update Documentation
**Problem:** Code is refactored, but AGENTS.md and docs/ still reference old structure.
**Result:** AI/humans get confused by outdated documentation.
**Solution:** Include "Documentation Phase" at the end of every refactoring plan.
### ❌ Ignore the Planning Directory
**Problem:** "I'll just create the plan in docs/ directly..."
**Result:** Git history polluted with draft iterations, or pressure to "commit something" too early.
**Solution:** Always use `planning/` for work-in-progress. Move to `docs/` only when done.
## Integration with AI Development
This project uses AI heavily (GitHub Copilot, Claude). The planning process supports AI development:
**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)
**AI updates:**
- `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
See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance.
## Tools and Resources
### Planning Directory
- `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)
### Helper Scripts
```bash
./scripts/lint-check # Verify code quality
./scripts/develop # Start HA for testing
./scripts/lint # Auto-fix issues
```
## FAQ
### Q: When should I create a plan vs. just start coding?
**A:** If you're asking this question, you probably need a plan. 😊
Simple rule: If you can't describe the entire change in 3 sentences, create a plan.
### Q: How detailed should the plan be?
**A:** Detailed enough to execute without major surprises, but not a line-by-line script.
Good plan level:
- 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
Too vague:
- "Refactor sensor.py to be better"
- No phase breakdown
- No testing strategy
### Q: What if the plan changes during implementation?
**A:** Update the plan! Planning documents are living documents.
If you discover:
- 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).
### Q: Should every refactoring follow this process?
**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
### Q: How do I know when a refactoring is successful?
**A:** Check the "Success Criteria" from your plan:
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
If you can't tick all boxes, the refactoring isn't done.
## Summary
**Key takeaways:**
1. **Plan when scope is unclear** (>500 lines, >5 files, breaking changes)
2. **Use planning/ directory** for free iteration (git-ignored)
3. **Work in phases** and test after each phase
4. **Document file lifecycle** (CREATE/MODIFY/DELETE/RENAME)
5. **Update documentation** after completion (AGENTS.md, docs/)
6. **Archive or delete** plan after implementation
**Remember:** Good planning prevents half-finished refactorings and makes rollback easier when things go wrong.
---
**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

View file

@ -0,0 +1,365 @@
---
comments: false
---
# Release Notes Generation
This project supports **three ways** to generate release notes from conventional commits, plus **automatic version management**.
## 🚀 Quick Start: Preparing a Release
**Recommended workflow (automatic & foolproof):**
```bash
# 1. Use the helper script to prepare release
./scripts/release/prepare 0.3.0
# This will:
# - Update manifest.json version to 0.3.0
# - Create commit: "chore(release): bump version to 0.3.0"
# - Create tag: v0.3.0
# - Show you what will be pushed
# 2. Review and push when ready
git push origin main v0.3.0
# 3. CI/CD automatically:
# - Detects the new tag
# - Generates release notes (excluding version bump commit)
# - Creates GitHub release
```
**If you forget to bump manifest.json:**
```bash
# Just edit manifest.json manually and commit
vim custom_components/tibber_prices/manifest.json # "version": "0.3.0"
git commit -am "chore(release): bump version to 0.3.0"
git push
# Auto-Tag workflow detects manifest.json change and creates tag automatically!
# Then Release workflow kicks in and creates the GitHub release
```
---
## 📋 Release Options
### 1. GitHub UI Button (Easiest)
Use GitHub's built-in release notes generator:
1. Go to [Releases](https://github.com/jpawlowski/hass.tibber_prices/releases)
2. Click "Draft a new release"
3. Select your tag
4. Click "Generate release notes" button
5. Edit if needed and publish
**Uses:** `.github/release.yml` configuration
**Best for:** Quick releases, works with PRs that have labels
**Note:** Direct commits appear in "Other Changes" category
---
### 2. Local Script (Intelligent)
Run `./scripts/release/generate-notes` to parse conventional commits locally.
**Automatic backend detection:**
```bash
# Generate from latest tag to HEAD
./scripts/release/generate-notes
# Generate between specific tags
./scripts/release/generate-notes v1.0.0 v1.1.0
# Generate from tag to HEAD
./scripts/release/generate-notes v1.0.0 HEAD
```
**Force specific backend:**
```bash
# Use AI (GitHub Copilot CLI)
RELEASE_NOTES_BACKEND=copilot ./scripts/release/generate-notes
# Use git-cliff (template-based)
RELEASE_NOTES_BACKEND=git-cliff ./scripts/release/generate-notes
# Use manual parsing (grep/awk fallback)
RELEASE_NOTES_BACKEND=manual ./scripts/release/generate-notes
```
**Disable AI** (useful for CI/CD):
```bash
USE_AI=false ./scripts/release/generate-notes
```
#### Backend Priority
The script automatically selects the best available backend:
1. **GitHub Copilot CLI** - AI-powered, context-aware (best quality)
2. **git-cliff** - Fast Rust tool with templates (reliable)
3. **Manual** - Simple grep/awk parsing (always works)
In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled.
#### Installing Optional Backends
**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`
Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and git-cliff will be available.
**Manual installation (outside DevContainer):**
**git-cliff** (template-based):
```bash
# See: https://git-cliff.org/docs/installation
# macOS
brew install git-cliff
# Cargo (all platforms)
cargo install git-cliff
# Manual binary download
wget https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-x86_64-unknown-linux-gnu.tar.gz
tar -xzf git-cliff-*.tar.gz
sudo mv git-cliff-*/git-cliff /usr/local/bin/
```
---
### 3. CI/CD Automation
Automatic release notes on tag push.
**Workflow:** `.github/workflows/release.yml`
**Triggers:** Version tags (`v1.0.0`, `v2.1.3`, etc.)
```bash
# Create and push a tag to trigger automatic release
git tag v1.0.0
git push origin v1.0.0
# GitHub Actions will:
# 1. Detect the new tag
# 2. Generate release notes using git-cliff
# 3. Create a GitHub release automatically
```
**Backend:** Uses `git-cliff` (AI disabled in CI for reliability)
---
## 📝 Output Format
All methods produce GitHub-flavored Markdown with emoji categories:
```markdown
## 🎉 New Features
- **scope**: Description ([abc1234](link-to-commit))
## 🐛 Bug Fixes
- **scope**: Description ([def5678](link-to-commit))
## 📚 Documentation
- **scope**: Description ([ghi9012](link-to-commit))
## 🔧 Maintenance & Refactoring
- **scope**: Description ([jkl3456](link-to-commit))
## 🧪 Testing
- **scope**: Description ([mno7890](link-to-commit))
```
---
## 🎯 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 |
---
## 🔄 Complete Release Workflows
### Workflow A: Using Helper Script (Recommended)
```bash
# Step 1: Prepare release (all-in-one)
./scripts/release/prepare 0.3.0
# Step 2: Review changes
git log -1 --stat
git show v0.3.0
# Step 3: Push when ready
git push origin main v0.3.0
# Done! CI/CD creates the release automatically
```
**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
---
### Workflow B: Manual (with Auto-Tag Safety Net)
```bash
# Step 1: Bump version manually
vim custom_components/tibber_prices/manifest.json
# Change: "version": "0.3.0"
# Step 2: Commit
git commit -am "chore(release): bump version to 0.3.0"
git push
# Step 3: Wait for Auto-Tag workflow
# GitHub Actions automatically creates v0.3.0 tag
# Then Release workflow creates the release
```
**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
---
### Workflow C: Manual Tag (Old Way)
```bash
# Step 1: Bump version
vim custom_components/tibber_prices/manifest.json
git commit -am "chore(release): bump version to 0.3.0"
# Step 2: Create tag manually
git tag v0.3.0
git push origin main v0.3.0
# Release workflow creates release
```
**What happens:**
1. You create and push tag manually
2. Release workflow creates release
3. Auto-Tag workflow skips (tag already exists)
---
## ⚙️ Configuration Files
- `scripts/release/prepare` - Helper script to bump version + create tag
- `.github/workflows/auto-tag.yml` - Automatic tag creation on manifest.json change
- `.github/workflows/release.yml` - Automatic release on tag push
- `.github/release.yml` - GitHub UI button configuration
- `cliff.toml` - git-cliff template (filters out version bumps)
---
## 🛡️ 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.
---
## 🐛 Troubleshooting
**"Tag already exists" error:**
```bash
# Local tag
git tag -d v0.3.0
# Remote tag (only if you need to recreate)
git push origin :refs/tags/v0.3.0
```
**Manifest version doesn't match tag:**
This shouldn't happen with the new workflows, but if it does:
```bash
# 1. Fix manifest.json
vim custom_components/tibber_prices/manifest.json
# 2. Amend the commit
git commit --amend -am "chore(release): bump version to 0.3.0"
# 3. Move the tag
git tag -f v0.3.0
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
---
## 🔍 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 `<ha-alert>` tags if needed
---
## 💡 Tips
1. **Conventional Commits:** Use proper commit format for best results:
```
feat(scope): Add new feature
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
3. **Test Locally:** Run `./scripts/release/generate-notes` before creating release
4. **AI vs Template:** GitHub Copilot CLI provides better descriptions, git-cliff is faster and more reliable
5. **CI/CD:** Tag push triggers automatic release - no manual intervention needed

View file

@ -0,0 +1,330 @@
# Repairs System
The Tibber Prices integration includes a proactive repair notification system that alerts users to important issues requiring attention. This system leverages Home Assistant's built-in `issue_registry` to create user-facing notifications in the UI.
## Overview
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
- **Non-blocking**: Integration continues to work even with active repairs
## Implemented Repair Types
### 1. Tomorrow Data Missing
**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
**User impact:**
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"])
await self._repair_manager.check_tomorrow_data_availability(
has_tomorrow_data=has_tomorrow_data,
current_time=current_time,
)
```
**Translation placeholders:**
- `home_name`: Name of the affected home
- `warning_hour`: Hour after which warning appears (default: 18)
### 2. Rate Limit Exceeded
**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
**User impact:**
API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires.
**Implementation:**
```python
# In error handler
is_rate_limit = (
"429" in error_str
or "rate limit" in error_str
or "too many requests" in error_str
)
if is_rate_limit:
await self._repair_manager.track_rate_limit_error()
# On successful update
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
### 3. Home Not Found
**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)
**User impact:**
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)
if not home_exists:
await self._repair_manager.create_home_not_found_repair()
else:
await self._repair_manager.clear_home_not_found_repair()
```
**Translation placeholders:**
- `home_name`: Name of the missing home
- `entry_id`: Config entry ID for reference
## Configuration Constants
Defined in `coordinator/constants.py`:
```python
TOMORROW_DATA_WARNING_HOUR = 18 # Hour after which to warn about missing tomorrow data
RATE_LIMIT_WARNING_THRESHOLD = 3 # Number of consecutive errors before creating repair
```
## Architecture
### Class Structure
```python
class TibberPricesRepairManager:
"""Manages repair issues for a single Tibber home."""
def __init__(
self,
hass: HomeAssistant,
entry_id: str,
home_name: str,
) -> None:
"""Initialize repair manager."""
self._hass = hass
self._entry_id = entry_id
self._home_name = home_name
# State tracking
self._tomorrow_data_repair_active = False
self._rate_limit_error_count = 0
self._rate_limit_repair_active = False
self._home_not_found_repair_active = False
```
### State Tracking
Each repair type maintains internal state to avoid redundant operations:
- **`_tomorrow_data_repair_active`**: Boolean flag, prevents creating duplicate repairs
- **`_rate_limit_error_count`**: Integer counter, tracks consecutive errors
- **`_rate_limit_repair_active`**: Boolean flag, tracks repair status
- **`_home_not_found_repair_active`**: Boolean flag, one-time repair (manual cleanup)
### Lifecycle Integration
**Coordinator Initialization:**
```python
self._repair_manager = TibberPricesRepairManager(
hass=hass,
entry_id=self.config_entry.entry_id,
home_name=self._home_name,
)
```
**Update Cycle Integration:**
```python
# Success path - check conditions
if result and "priceInfo" in result:
has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"])
await self._repair_manager.check_tomorrow_data_availability(
has_tomorrow_data=has_tomorrow_data,
current_time=current_time,
)
await self._repair_manager.clear_rate_limit_tracking()
# Error path - track rate limits
if is_rate_limit:
await self._repair_manager.track_rate_limit_error()
```
**Shutdown Cleanup:**
```python
async def async_shutdown(self) -> None:
"""Shut down coordinator and clean up."""
await self._repair_manager.clear_all_repairs()
# ... other cleanup ...
```
## Translation System
Repairs use Home Assistant's standard translation system. Translations are defined in:
- `/translations/en.json`
- `/translations/de.json`
- `/translations/nb.json`
- `/translations/nl.json`
- `/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"
}
}
}
```
## 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
## Testing Repairs
### Tomorrow Data Missing
1. Wait until after 18:00 local time
2. Ensure integration has no tomorrow price data
3. Repair should appear in UI
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
```
### Rate Limit Exceeded
1. Simulate 3+ consecutive rate limit errors
2. Repair should appear after 3rd error
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
### Home Not Found
1. Remove home from Tibber account via app/web
2. Wait for user data refresh (daily check)
3. Repair appears indicating home is missing
4. Remove integration entry to clear repair
## Adding New Repair Types
To add a new repair type:
1. **Add constants** (if needed) in `coordinator/constants.py`
2. **Add state tracking** in `TibberPricesRepairManager.__init__`
3. **Implement check method** with create/clear logic
4. **Add translations** to all 5 language files
5. **Integrate into coordinator** update cycle or error handlers
6. **Add cleanup** to `clear_all_repairs()` method
7. **Document** in this file
**Example template:**
```python
async def check_new_condition(self, *, param: bool) -> None:
"""Check new condition and create/clear repair."""
should_warn = param # Your condition logic
if should_warn and not self._new_repair_active:
await self._create_new_repair()
elif not should_warn and self._new_repair_active:
await self._clear_new_repair()
async def _create_new_repair(self) -> None:
"""Create new repair issue."""
_LOGGER.warning("New issue detected - creating repair")
ir.async_create_issue(
self._hass,
DOMAIN,
f"new_issue_{self._entry_id}",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="new_issue",
translation_placeholders={
"home_name": self._home_name,
},
)
self._new_repair_active = True
async def _clear_new_repair(self) -> None:
"""Clear new repair issue."""
_LOGGER.debug("New issue resolved - clearing repair")
ir.async_delete_issue(
self._hass,
DOMAIN,
f"new_issue_{self._entry_id}",
)
self._new_repair_active = False
```
## Best Practices
1. **Always use state tracking** - Prevents duplicate repair creation
2. **Auto-clear when resolved** - Improves user experience
3. **Clear on shutdown** - Prevents orphaned repairs
4. **Use descriptive issue IDs** - Include entry_id for multi-home setups
5. **Provide actionable guidance** - Tell users what they can do
6. **Use appropriate severity** - WARNING for most cases, ERROR only for critical
7. **Test all language translations** - Ensure placeholders work correctly
8. **Document expected behavior** - What triggers, what clears, what user should do
## Future Enhancements
Potential additions to the repairs system:
- **Stale data warning**: Alert when cache is >24 hours old with no API updates
- **Missing permissions**: Detect insufficient API token scopes
- **Config migration needed**: Notify users of breaking changes requiring reconfiguration
- **Extreme price alert**: Warn when prices exceed historical thresholds (optional, user-configurable)
## References
- Home Assistant Repairs Documentation: https://developers.home-assistant.io/docs/core/platform/repairs
- Issue Registry API: `homeassistant.helpers.issue_registry`
- Integration Constants: `custom_components/tibber_prices/const.py`
- Repair Manager Implementation: `custom_components/tibber_prices/coordinator/repairs.py`

View file

@ -0,0 +1,57 @@
# Development Setup
> **Note:** This guide is under construction. For now, please refer to [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md) for detailed setup information.
## Prerequisites
- VS Code with Dev Container support
- Docker installed and running
- GitHub account (for Tibber API token)
## Quick Setup
```bash
# Clone the repository
git clone https://github.com/jpawlowski/hass.tibber_prices.git
cd hass.tibber_prices
# Open in VS Code
code .
# Reopen in DevContainer (VS Code will prompt)
# Or manually: Ctrl+Shift+P → "Dev Containers: Reopen in Container"
```
## Development Environment
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
## Running the Integration
```bash
# Start Home Assistant in debug mode
./scripts/develop
```
Visit http://localhost:8123
## Making Changes
```bash
# Lint and format code
./scripts/lint
# Check-only (CI mode)
./scripts/lint-check
# Validate integration structure
./scripts/release/hassfest
```
See [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md) for detailed patterns and conventions.

View file

@ -0,0 +1,52 @@
# Testing
> **Note:** This guide is under construction.
## Integration Validation
Before running tests or committing changes, validate the integration structure:
```bash
# Run local validation (JSON syntax, Python syntax, required files)
./scripts/release/hassfest
```
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
**Note:** Full hassfest validation runs in GitHub Actions on push.
## Running Tests
```bash
# Run all tests
pytest tests/
# Run specific test file
pytest tests/test_coordinator.py
# Run with coverage
pytest --cov=custom_components.tibber_prices tests/
```
## Manual Testing
```bash
# Start development environment
./scripts/develop
```
Then test in Home Assistant UI:
- Configuration flow
- Sensor states and attributes
- Services
- Translation strings
## Test Guidelines
Coming soon...

View file

@ -0,0 +1,433 @@
---
comments: false
---
# Timer Architecture
This document explains the timer/scheduler system in the Tibber Prices integration - what runs when, why, and how they coordinate.
## Overview
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()` |
**Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**.
---
## Timer #1: DataUpdateCoordinator (HA Built-in)
**File:** `coordinator/core.py``TibberPricesDataUpdateCoordinator`
**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)
**Purpose:** Check if fresh API data is needed, fetch if necessary
**What it does:**
```python
async def _async_update_data(self) -> TibberPricesData:
# Step 1: Check midnight turnover FIRST (prevents race with Timer #2)
if self._check_midnight_turnover_needed(dt_util.now()):
await self._perform_midnight_data_rotation(dt_util.now())
# Notify ALL entities after midnight turnover
return self.data # Early return
# Step 2: Check if we need tomorrow data (after 13:00)
if self._should_update_price_data() == "tomorrow_check":
await self._fetch_and_update_data() # Fetch from API
return self.data
# Step 3: Use cached data (fast path - most common)
return self.data
```
**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
- Handles backpressure (won't queue up if previous update still running)
---
## Timer #2: Quarter-Hour Refresh (Custom)
**File:** `coordinator/listeners.py``ListenerManager.schedule_quarter_hour_refresh()`
**Type:** Custom timer using `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)`
**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
**What it does:**
```python
async def _handle_quarter_hour_refresh(self, now: datetime) -> None:
# Step 1: Check midnight turnover (coordinates with Timer #1)
if self._check_midnight_turnover_needed(now):
# Timer #1 might have already done this → atomic check handles it
await self._perform_midnight_data_rotation(now)
# Notify ALL entities after midnight turnover
return
# Step 2: Normal quarter-hour refresh (most common path)
# Only notify time-sensitive entities (current_interval_price, etc.)
self._listener_manager.async_update_time_sensitive_listeners()
```
**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
---
## Timer #3: Minute Refresh (Custom)
**File:** `coordinator/listeners.py``ListenerManager.schedule_minute_refresh()`
**Type:** Custom timer using `async_track_utc_time_change(second=0)` (every minute)
**Purpose:** Update countdown and progress sensors for smooth UX
**What it does:**
```python
async def _handle_minute_refresh(self, now: datetime) -> None:
# Only notify minute-update entities
# No data fetching, no transformation, no midnight handling
self._listener_manager.async_update_minute_listeners()
```
**Which entities listen:**
- `best_price_remaining_minutes` - Countdown timer
- `peak_price_remaining_minutes` - Countdown timer
- `best_price_progress` - Progress bar (0-100%)
- `peak_price_progress` - Progress bar (0-100%)
- ~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)
---
## Listener Pattern (Python/HA Terminology)
**Your question:** "Sind Timer für dich eigentlich 'Listener'?"
**Answer:** In Home Assistant terminology:
- **Timer** = The mechanism that triggers at specific times (`async_track_utc_time_change`)
- **Listener** = A callback function that gets called when timer triggers
- **Observer Pattern** = Entities register callbacks, coordinator notifies them
**How it works:**
```python
# Entity registers a listener callback
class TibberPricesSensor(CoordinatorEntity):
async def async_added_to_hass(self):
# Register this entity's update callback
self._remove_listener = self.coordinator.async_add_time_sensitive_listener(
self._handle_coordinator_update
)
# Coordinator maintains list of listeners
class ListenerManager:
def __init__(self):
self._time_sensitive_listeners = [] # List of callbacks
def async_add_time_sensitive_listener(self, callback):
self._time_sensitive_listeners.append(callback)
def async_update_time_sensitive_listeners(self):
# Timer triggered → notify all listeners
for callback in self._time_sensitive_listeners:
callback() # Entity updates itself
```
**Why this pattern:**
- Decouples timer logic from entity logic
- One timer can notify many entities efficiently
- Entities can unregister when removed (cleanup)
- Standard HA pattern for coordinator-based integrations
---
## Timer Coordination Scenarios
### Scenario 1: Normal Operation (No Midnight)
```
14:00:00 → Timer #2 triggers
→ Update time-sensitive entities (current price changed)
→ 60 entities updated (~5ms)
14:03:12 → Timer #1 triggers (HA's 15-min cycle)
→ Check if tomorrow data needed (no, still cached)
→ Return cached data (fast path, ~2ms)
14:15:00 → Timer #2 triggers
→ Update time-sensitive entities
→ 60 entities updated (~5ms)
14:16:00 → Timer #3 triggers
→ Update countdown/progress entities
→ 10 entities updated (~1ms)
```
**Key observation:** Timer #1 and Timer #2 run **independently**, no conflicts.
### Scenario 2: Midnight Turnover
```
23:45:12 → Timer #1 triggers
→ Check midnight: current_date=2025-11-17, last_check=2025-11-17
→ No turnover needed
→ Return cached data
00:00:00 → Timer #2 triggers FIRST (synchronized to midnight)
→ Check midnight: current_date=2025-11-18, last_check=2025-11-17
→ Turnover needed! Perform rotation, save cache
→ _last_midnight_check = 2025-11-18
→ Notify ALL entities
00:03:12 → Timer #1 triggers (its regular cycle)
→ Check midnight: current_date=2025-11-18, last_check=2025-11-18
→ Turnover already done → skip
→ Return existing data (fast path)
```
**Key observation:** Atomic date comparison prevents double-turnover, whoever runs first wins.
### Scenario 3: Tomorrow Data Check (After 13:00)
```
13:00:00 → Timer #2 triggers
→ Normal quarter-hour refresh
→ Update time-sensitive entities
13:03:12 → Timer #1 triggers
→ Check tomorrow data: missing or invalid
→ Fetch from Tibber API (~300ms)
→ Transform data (~200ms)
→ Calculate periods (~100ms)
→ Notify ALL entities (new data available)
13:15:00 → Timer #2 triggers
→ Normal quarter-hour refresh (uses newly fetched data)
→ Update time-sensitive entities
```
**Key observation:** Timer #1 does expensive work (API + transform), Timer #2 does cheap work (entity notify).
---
## Why We Keep HA's Timer (Timer #1)
**Your question:** "warum wir den HA timer trotzdem weiter benutzen, da er ja für uns unkontrollierte aktualisierte änderungen triggert"
**Answer:** You're correct that it's not synchronized, but that's actually **intentional**:
### 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, ...
- ✅ Natural distribution over ~30 minutes
- ✅ Plus: Random 0-30s delay on tomorrow checks
**Result:** API load spread evenly, no spikes.
### Reason 2: What Timer #1 Actually Checks
Timer #1 does NOT blindly update. It checks:
```python
def _should_update_price_data(self) -> str:
# Check 1: Do we have tomorrow data? (only relevant after ~13:00)
if tomorrow_missing or tomorrow_invalid:
return "tomorrow_check" # Fetch needed
# Check 2: Is cache still valid?
if cache_valid:
return "cached" # No fetch needed (most common!)
# Check 3: Has enough time passed?
if time_since_last_update < threshold:
return "cached" # Too soon, skip fetch
return "update_needed" # Rare case
```
**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
### Reason 3: HA Integration Best Practices
- ✅ Standard HA pattern: `DataUpdateCoordinator` is recommended by HA docs
- ✅ Automatic retry logic for temporary API failures
- ✅ Backpressure handling (won't queue updates if previous still running)
- ✅ Developer tools integration (users can manually trigger refresh)
- ✅ Diagnostics integration (shows last update time, success/failure)
### What We DO Synchronize
- ✅ **Timer #2:** Entity state updates at exact boundaries (user-visible)
- ✅ **Timer #3:** Countdown/progress at exact minutes (user-visible)
- ❌ **Timer #1:** API fetch timing (invisible to user, distribution wanted)
---
## 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)
- **Frequency:** ~96 times/day
- **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
- **Lightweight:** Just countdown math
**Total CPU budget:** ~15 seconds/day for all timers combined.
---
## Debugging Timer Issues
### Check Timer #1 (HA Coordinator)
```python
# Enable debug logging
_LOGGER.setLevel(logging.DEBUG)
# Watch for these log messages:
"Fetching data from API (reason: tomorrow_check)" # API call
"Using cached data (no update needed)" # Fast path
"Midnight turnover detected (Timer #1)" # Turnover
```
### Check Timer #2 (Quarter-Hour)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```
### Check Timer #3 (Minute)
```python
# Watch coordinator logs:
"Updated 10 minute-update entities" # Every minute
```
### Common Issues
1. **Timer #2 not triggering:**
- 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?
3. **API overload:**
- Check: Random delay working? (0-30s jitter on tomorrow check)
- Check: Cache validation logic correct?
---
## Related Documentation
- **[Architecture](./architecture.md)** - Overall system design, data flow
- **[Caching Strategy](./caching-strategy.md)** - Cache lifetimes, invalidation, midnight turnover
- **[AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.29.0/AGENTS.md)** - Complete reference for AI development
---
## 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

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -0,0 +1,61 @@
{
"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
}
]
}

View file

@ -1,4 +1,5 @@
[
"v0.29.0",
"v0.28.0",
"v0.27.0",
"v0.24.0",

View file

@ -4,6 +4,30 @@ Home Assistant now surfaces these backend service endpoints as **Actions** in th
You can still call them from automations, scripts, and dashboards the same way as before (`service: tibber_prices.get_chartdata`, etc.), just remember that the frontend officially lists them as actions.
## Finding Your Entry ID
Every action requires an `entry_id` parameter that tells Home Assistant which Tibber home (integration instance) to use. If you only have one home, there is still exactly one entry ID — you just need to know where to find it.
### In the Action UI — no lookup needed
When you use the action through the Home Assistant interface (Developer Tools → Actions, or the Action picker inside the automation / script editor), the `entry_id` field renders as a **dropdown list** showing all your configured Tibber Prices instances. Just select your home from the drop-down and Home Assistant fills in the correct ID automatically. You never have to deal with the raw ID string.
### In YAML — copy from the integration menu
When you write YAML directly (automations, scripts, Lovelace dashboard cards), you need the actual ID string. The quickest way to get it:
1. Go to **Settings → Devices & Services**
2. Find the **Tibber Prices** integration card
3. Click the **⋮** (three-dot) menu on the card
4. Choose **"Copy Config Entry ID"**
5. Paste the value wherever you see `YOUR_ENTRY_ID` in the YAML examples
The ID looks like a long alphanumeric string, for example `01JKPC7AB3EF4GH5IJ6KL7MN8P`.
:::tip Multiple homes?
If you have configured more than one Tibber home, each home has its own entry ID. Repeat the steps above for each integration card to get the individual IDs.
:::
## Available Actions
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).

View file

@ -1,7 +1,5 @@
# Automation Examples
> **Note:** This guide is under construction.
> **Tip:** For dashboard examples with dynamic icons and colors, see the **[Dynamic Icons Guide](dynamic-icons.md)** and **[Dynamic Icon Colors Guide](icon-colors.md)**.
## Table of Contents
@ -25,7 +23,245 @@
## Price-Based Automations
Coming soon...
### Understanding V-Shaped Price Days
Some days have a **V-shaped** or **U-shaped** price curve: prices drop to very cheap levels (often rated VERY_CHEAP) for an extended period, then rise again. This is common during sunny midday hours (solar surplus) or low-demand nights.
**The challenge:** The Best Price Period might only cover 12 hours (the absolute cheapest window), but prices could remain favorable for 46 hours. If you only rely on the Best Price Period binary sensor, you miss out on the surrounding cheap hours.
**The solution:** Combine multiple sensors to ride the full cheap wave:
```mermaid
gantt
title V-Shaped Price Day — Best Price vs Full Cheap Window
dateFormat HH:mm
axisFormat %H:%M
section Price Level
NORMAL :done, n1, 06:00, 2h
CHEAP :active, c1, 08:00, 2h
VERY CHEAP :crit, vc, 10:00, 2h
CHEAP :active, c2, 12:00, 2h
NORMAL :done, n2, 14:00, 2h
EXPENSIVE : e1, 16:00, 2h
section Detected
Best Price Period :crit, bp, 10:00, 2h
section Your Goal
Run while cheap + trend OK :active, goal, 08:00, 6h
```
> **Key insight:** The Best Price Period covers only the absolute minimum (2h). By combining the period sensor with price level and trend, you can extend device runtime to the full 6h cheap window.
### Use Case: Ride the Full Cheap Wave
This automation starts a flexible load when the best price period begins, but keeps it running as long as prices remain favorable — even after the period ends.
```yaml
automation:
- alias: "Heat Pump - Extended Cheap Period"
description: "Run heat pump during the full cheap price window, not just best-price period"
mode: restart
trigger:
# Start: Best price period begins
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
id: best_price_on
# Re-evaluate: Every 15 minutes while running
- platform: state
entity_id: sensor.<home_name>_current_electricity_price
id: price_update
condition:
# Continue running while EITHER condition is true:
- condition: or
conditions:
# Path 1: We're in a best price period
- condition: state
entity_id: binary_sensor.<home_name>_best_price_period
state: "on"
# Path 2: Price is still cheap AND trend is not rising
- condition: and
conditions:
- condition: template
value_template: >
{% set level = state_attr('sensor.<home_name>_current_electricity_price', 'rating_level') %}
{{ level in ['VERY_CHEAP', 'CHEAP'] }}
- condition: template
value_template: >
{% set trend = state_attr('sensor.<home_name>_current_price_trend', 'trend_value') | int(0) %}
{{ trend <= 0 }}
action:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 22
```
**How it works:**
1. Starts when the best price period triggers
2. On each price update, rechecks conditions
3. Keeps running while prices are CHEAP or VERY_CHEAP **and** the trend is not rising
4. Stops when either prices climb above CHEAP or the trend turns to rising
### Use Case: Pre-Emptive Start Before Best Price
Use the trend to start slightly before the cheapest period — useful for appliances with warm-up time:
```yaml
automation:
- alias: "Water Heater - Pre-Heat Before Cheapest"
trigger:
- platform: state
entity_id: sensor.<home_name>_current_electricity_price
condition:
# Conditions: Prices are falling AND we're approaching cheap levels
- condition: template
value_template: >
{% set trend_value = state_attr('sensor.<home_name>_price_trend_3h', 'trend_value') | int(0) %}
{% set level = state_attr('sensor.<home_name>_current_electricity_price', 'rating_level') %}
{{ trend_value <= -1 and level in ['CHEAP', 'NORMAL'] }}
# AND: The next 3 hours will be cheaper on average
- condition: template
value_template: >
{% set current = states('sensor.<home_name>_current_electricity_price') | float %}
{% set future_avg = state_attr('sensor.<home_name>_price_trend_3h', 'next_3h_avg') | float %}
{{ future_avg < current }}
action:
- service: water_heater.set_temperature
target:
entity_id: water_heater.boiler
data:
temperature: 60
```
### Use Case: Protect Against Rising Prices
Stop or reduce consumption when prices are climbing:
```yaml
automation:
- alias: "EV Charger - Stop When Prices Rising"
trigger:
- platform: template
value_template: >
{{ state_attr('sensor.<home_name>_current_price_trend', 'trend_value') | int(0) >= 1 }}
condition:
# Only stop if price is already above typical level
- condition: template
value_template: >
{% set level = state_attr('sensor.<home_name>_current_electricity_price', 'rating_level') %}
{{ level in ['NORMAL', 'EXPENSIVE', 'VERY_EXPENSIVE'] }}
action:
- service: switch.turn_off
target:
entity_id: switch.ev_charger
- service: notify.mobile_app
data:
message: >
EV charging paused — prices are {{ states('sensor.<home_name>_current_price_trend') }}
and currently at {{ states('sensor.<home_name>_current_electricity_price') }}
{{ state_attr('sensor.<home_name>_current_electricity_price', 'unit_of_measurement') }}.
Next trend change in ~{{ state_attr('sensor.<home_name>_next_price_trend_change', 'minutes_until_change') }} minutes.
```
### Use Case: Multi-Window Trend Strategy for Flexible Loads
Combine short-term and long-term trend sensors for smarter decisions. This example manages a heat pump boost:
- If **both** windows say `rising` → prices only go up from here, boost now
- If short-term is `falling` but long-term is `rising` → brief dip coming, wait for it then boost
- If **both** say `falling` → prices are dropping, definitely wait
- If long-term says `falling` → cheaper hours ahead, no rush
```yaml
automation:
- alias: "Heat Pump - Smart Boost Using Multi-Window Trends"
description: >
Combines 1h (short-term) and 6h (long-term) trend windows.
Rising = current price is LOWER than future average = act now.
Falling = current price is HIGHER than future average = wait.
trigger:
- platform: state
entity_id: sensor.<home_name>_price_trend_1h
- platform: state
entity_id: sensor.<home_name>_price_trend_6h
condition:
# Only consider if best price period is NOT active
# (if it IS active, a separate automation handles it)
- condition: state
entity_id: binary_sensor.<home_name>_best_price_period
state: "off"
action:
- choose:
# Case 1: Both rising → prices only go up, boost NOW
- conditions:
- condition: template
value_template: >
{% set t1 = state_attr('sensor.<home_name>_price_trend_1h', 'trend_value') | int(0) %}
{% set t6 = state_attr('sensor.<home_name>_price_trend_6h', 'trend_value') | int(0) %}
{{ t1 >= 1 and t6 >= 1 }}
sequence:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 22
# Case 2: 1h falling + 6h rising → brief dip, wait then act
- conditions:
- condition: template
value_template: >
{% set t1 = state_attr('sensor.<home_name>_price_trend_1h', 'trend_value') | int(0) %}
{% set t6 = state_attr('sensor.<home_name>_price_trend_6h', 'trend_value') | int(0) %}
{{ t1 <= -1 and t6 >= 1 }}
sequence:
# Short-term dip — wait for it to bottom out
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 20
# Case 3: 6h falling → cheaper hours ahead, reduce now
- conditions:
- condition: template
value_template: >
{% set t6 = state_attr('sensor.<home_name>_price_trend_6h', 'trend_value') | int(0) %}
{{ t6 <= -1 }}
sequence:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 19
# Default: stable on both → maintain normal operation
default:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 20.5
```
:::tip Why "rising" means "act now"
A common misconception: **"rising" does NOT mean "too late"**. It means your current price is **lower** than the future average — so right now is actually a good time. See [How to Use Trend Sensors for Decisions](sensors.md#how-to-use-trend-sensors-for-decisions) in the sensor documentation for details.
:::
### 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` |
---
@ -158,7 +394,66 @@ automation:
## Best Hour Detection
Coming soon...
### Use Case: Find the Best Time to Run an Appliance
Use future average sensors to determine the cheapest upcoming window for a timed appliance (e.g., dishwasher with 2-hour ECO program):
```yaml
automation:
- alias: "Dishwasher - Schedule for Cheapest 2h Window"
trigger:
# Check when tomorrow's data arrives (typically 13:00-14:00)
- platform: state
entity_id: sensor.<home_name>_price_tomorrow
attribute: price_mean
condition:
# Only if tomorrow data is available
- condition: template
value_template: >
{{ state_attr('sensor.<home_name>_price_tomorrow', 'price_mean') is not none }}
action:
# Compare different future windows to find cheapest start
- variables:
next_2h: "{{ state_attr('sensor.<home_name>_price_trend_2h', 'next_2h_avg') | float(999) }}"
next_4h: "{{ state_attr('sensor.<home_name>_price_trend_4h', 'next_4h_avg') | float(999) }}"
daily_avg: "{{ state_attr('sensor.<home_name>_price_today', 'price_median') | float(999) }}"
- service: notify.mobile_app
data:
title: "Dishwasher Scheduling"
message: >
Next 2h avg: {{ next_2h }} ct/kWh
Next 4h avg: {{ next_4h }} ct/kWh
Today's typical: {{ daily_avg }} ct/kWh
{% if next_2h < daily_avg * 0.8 %}
→ Now is a great time to start!
{% else %}
→ Consider waiting for a cheaper window.
{% endif %}
```
### Use Case: Notify When Cheapest Window Starts
Get a push notification when the best price period begins:
```yaml
automation:
- alias: "Notify - Cheap Window Started"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
action:
- service: notify.mobile_app
data:
title: "⚡ Cheap Electricity Now!"
message: >
Best price period started.
Current price: {{ states('sensor.<home_name>_current_electricity_price') }}
{{ state_attr('sensor.<home_name>_current_electricity_price', 'unit_of_measurement') }}.
Duration: {{ state_attr('binary_sensor.<home_name>_best_price_period', 'duration_minutes') }} minutes.
Average period price: {{ state_attr('binary_sensor.<home_name>_best_price_period', 'price_mean') }}
{{ state_attr('sensor.<home_name>_current_electricity_price', 'unit_of_measurement') }}.
```
---
@ -172,6 +467,14 @@ Coming soon...
The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card configuration examples for visualizing electricity prices.
:::info Finding your Entry ID (`entry_id`)
The examples below contain `entry_id: YOUR_ENTRY_ID`. This value identifies which Tibber home (integration instance) the action targets.
**In the Action UI (Developer Tools → Actions or the automation editor):** The `entry_id` field is a **dropdown** — just select your Tibber home and HA fills in the correct ID automatically.
**In YAML:** Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** (three-dot) menu, and choose **"Copy Config Entry ID"**. Paste the copied value in place of `YOUR_ENTRY_ID`.
:::
### Prerequisites
**Required:**

View file

@ -6,6 +6,14 @@ This guide showcases the different chart configurations available through the `t
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
:::info Finding your Entry ID (`entry_id`)
Every example below contains `entry_id: YOUR_ENTRY_ID`. This value identifies which Tibber home (integration instance) the action targets.
**In the Action UI (Developer Tools → Actions or the automation editor):** The `entry_id` field is a **dropdown** — just select your Tibber home and HA fills in the correct ID automatically.
**In YAML:** Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** (three-dot) menu, and choose **"Copy Config Entry ID"**. Paste the copied value in place of `YOUR_ENTRY_ID`.
:::
## Overview
The integration can generate 4 different chart modes, each optimized for specific use cases:

View file

@ -2,6 +2,42 @@
Understanding the fundamental concepts behind the Tibber Prices integration.
## How Data Flows
```mermaid
flowchart LR
subgraph API["☁️ Tibber API"]
raw["Raw prices<br/>(quarter-hourly)"]
end
subgraph Integration["⚙️ Integration"]
direction TB
enrich["Enrichment<br/><small>24h averages, differences</small>"]
classify["Classification"]
enrich --> classify
end
subgraph Sensors["📊 Your Sensors"]
direction TB
prices["Price sensors<br/><small>current, min, max, avg</small>"]
ratings["Ratings & Levels<br/><small>LOW / NORMAL / HIGH</small>"]
periods["Periods<br/><small>best & peak windows</small>"]
trends["Trends & Volatility<br/><small>falling / stable / rising</small>"]
end
raw -->|every 15 min| enrich
classify --> prices
classify --> ratings
classify --> periods
classify --> trends
style API fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style Integration fill:#fff9e6,stroke:#ffb800,stroke-width:2px
style Sensors fill:#e6fff5,stroke:#00c853,stroke-width:2px
```
The integration fetches raw quarter-hourly prices from Tibber, enriches them with statistical context (averages, differences), and exposes the results as sensors you can use in automations and dashboards.
## Price Intervals
The integration works with **quarter-hourly intervals** (15 minutes):
@ -50,6 +86,17 @@ The integration enriches every interval with context:
This helps you understand if current prices are exceptional or typical.
## V-Shaped and U-Shaped Price Days
Some days show distinctive price curve shapes:
- **V-shaped**: Prices drop sharply, hit a brief minimum, then rise sharply again (common during short midday solar surplus)
- **U-shaped**: Prices drop to a low level and stay there for an extended period before rising (common during nighttime or extended low-demand periods)
**Why this matters:** On these days, the Best Price Period may be short (12 hours, covering only the absolute minimum), but prices can remain favorable for 46 hours. By combining [trend sensors](sensors.md#trend-sensors) with [price levels](sensors.md#core-price-sensors) in automations, you can ride the full cheap wave instead of only using the detected period.
See [Automation Examples → V-Shaped Days](automation-examples.md#understanding-v-shaped-price-days) for practical patterns.
## Multi-Home Support
You can add multiple Tibber homes to track prices for:

View file

@ -1,12 +1,131 @@
# Configuration
> **Note:** This guide is under construction. For detailed setup instructions, please refer to the [main README](https://github.com/jpawlowski/hass.tibber_prices/blob/main/README.md).
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
## Initial Setup
Coming soon...
After [installing](installation.md) the integration:
1. Go to **Settings → Devices & Services**
2. Click **+ Add Integration**
3. Search for **Tibber Price Information & Ratings**
4. **Enter your API token** from [developer.tibber.com](https://developer.tibber.com/settings/access-token)
5. **Select your Tibber home** from the dropdown (if you have multiple)
6. Click **Submit** — the integration starts fetching price data
The integration will immediately create sensors for your home. Data typically arrives within 12 minutes.
### Adding Additional Homes
If you have multiple Tibber homes (e.g., different locations):
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** → **Add another home**
3. Select the additional home from the dropdown
4. Each home gets its own set of sensors with unique entity IDs
## Options Flow (Configuration Wizard)
After initial setup, configure the integration through a multi-step wizard:
**Settings → Devices & Services → Tibber Prices → Configure**
```mermaid
flowchart LR
S1["① General"] --> S2["② Currency"]
S2 --> S3["③ Ratings"]
S3 --> S4["④ Levels"]
S4 --> S5["⑤ Volatility"]
S5 --> S6["⑥ Best Price"]
S6 --> S7["⑦ Peak Price"]
S7 --> S8["⑧ Trends"]
S8 --> S9["⑨ Chart"]
style S1 fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style S6 fill:#e6fff5,stroke:#00c853,stroke-width:2px
style S7 fill:#fff0f0,stroke:#ff5252,stroke-width:2px
```
All steps have sensible defaults — you can click through without changes and fine-tune later.
### Step 1: General Settings
- **Extended entity descriptions**: Show `description`, `long_description`, and `usage_tips` attributes on all sensors (useful for learning, can be disabled later to reduce attribute clutter)
- **Average sensor display**: Choose **Median** (typical price, spike-resistant) or **Mean** (mathematical average for cost calculations)
### Step 2: Currency Display
- **Base currency**: Shows prices as €/kWh, kr/kWh (e.g., 0.25 €/kWh)
- **Subunit**: Shows prices as ct/kWh, øre/kWh (e.g., 25.00 ct/kWh)
- Smart defaults: EUR → subunit (cents), NOK/SEK/DKK → base currency (kroner)
### Step 3: Price Rating Thresholds
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) |
### Step 4: Price Level Gap Tolerance
- **Gap tolerance** for Tibber's API-provided levels (VERY_CHEAP through VERY_EXPENSIVE)
- Smooths isolated level flickers: a single NORMAL surrounded by CHEAP → corrected to CHEAP
- Default: 1 interval tolerance
### Step 5: Price Volatility Thresholds
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 |
### 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
See [Period Calculation](period-calculation.md) for an in-depth explanation.
### Step 7: Peak Price Period
Mirrors Best Price configuration but for expensive windows. Detects periods to **avoid** consumption.
### Step 8: Price Trend Thresholds
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" |
Thresholds are [volatility-adaptive](sensors.md#trend-sensors): automatically widened on volatile days to prevent constant state changes.
### Step 9: Chart Data Export (Legacy)
Information page for the legacy chart data export sensor. For new setups, use the [get_chartdata action](actions.md) instead.
## Configuration Options

View file

@ -24,6 +24,11 @@ Quick reference for terms used throughout the documentation.
## C
**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"**.
**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.
@ -92,8 +97,17 @@ Quick reference for terms used throughout the documentation.
**Leading Average**
: Average price over the next 24 hours from current interval.
**Trend**
: Directional price movement indicator. Simple trends compare current price to future averages (1h12h). Current trend represents the ongoing price direction using a 3-hour outlook. Uses a 5-level scale: strongly_falling, falling, stable, rising, strongly_rising.
**Trend Hysteresis**
: Stability mechanism for trend change prediction. Requires 2 consecutive intervals confirming a different trend before reporting a change. Prevents false alarms from single-interval price spikes.
## V
**V-Shaped Day**
: Day with a V- or U-shaped price curve where prices drop to very cheap levels for an extended period. The Best Price Period covers only the absolute minimum, but favorable conditions may last much longer. See [V-Shaped Days](concepts.md#v-shaped-and-u-shaped-price-days).
**Volatility**
: Measure of price stability (LOW, MEDIUM, HIGH). High volatility = large price swings = good for timing optimization.

View file

@ -1,15 +1,69 @@
# Installation
> **Note:** This guide is under construction. For now, please refer to the [main README](https://github.com/jpawlowski/hass.tibber_prices/blob/main/README.md) for installation instructions.
## HACS Installation (Recommended)
Coming soon...
[HACS](https://hacs.xyz/) (Home Assistant Community Store) is the easiest way to install and keep the integration up to date.
### Prerequisites
- Home Assistant 2025.10.0 or newer
- [HACS](https://hacs.xyz/docs/use/) installed and configured
- A [Tibber API token](https://developer.tibber.com/settings/access-token)
### Steps
1. Open HACS in your Home Assistant sidebar
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**
5. Click **Add**
6. Find **Tibber Price Information & Ratings** in the integration list
7. Click **Download**
8. **Restart Home Assistant**
9. Continue with [Configuration](configuration.md)
### Updating
HACS will show a notification when updates are available:
1. Open HACS → **Integrations**
2. Find **Tibber Price Information & Ratings**
3. Click **Update**
4. **Restart Home Assistant**
## Manual Installation
Coming soon...
If you prefer not to use HACS:
## Configuration
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/
└── ...
```
4. **Restart Home Assistant**
5. Continue with [Configuration](configuration.md)
Coming soon...
## After Installation
Once installed and restarted, add the integration:
1. Go to **Settings → Devices & Services**
2. Click **+ Add Integration**
3. Search for **Tibber Price Information & Ratings**
4. Enter your [Tibber API token](https://developer.tibber.com/settings/access-token)
5. Select your Tibber home
6. The integration will start fetching price data
See the [Configuration Guide](configuration.md) for detailed setup options.

View file

@ -78,6 +78,27 @@ The integration sets different **initial defaults** because the features serve d
Each day, the integration analyzes all 96 quarter-hourly price intervals and identifies **continuous time ranges** that meet specific criteria.
```mermaid
flowchart TD
A["96 intervals per day"] --> B{"① Flexibility<br/><small>Close to MIN/MAX?</small>"}
B -->|Yes| C{"② Distance<br/><small>Meaningfully different<br/>from average?</small>"}
B -->|No| X1["❌ excluded"]
C -->|Yes| D{"③ Duration<br/><small>≥ 60 min?</small>"}
C -->|No| X2["❌ excluded"]
D -->|Yes| E{"④ Level filter<br/><small>(optional)</small>"}
D -->|No| X3["❌ too short"]
E -->|Pass| F["⑤ Spike smoothing"]
E -->|Fail| X4["❌ filtered"]
F --> G["✅ Period found"]
style A fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style G fill:#e6fff5,stroke:#00c853,stroke-width:2px
style X1 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
style X2 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
style X3 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
style X4 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
```
Think of it like this:
1. **Find potential windows** - Times close to the daily MIN (Best Price) or MAX (Peak Price)
@ -378,29 +399,43 @@ Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your conf
For each day, the system tries:
**Flexibility Levels (Attempts):**
```mermaid
flowchart TD
Start["Start: base flex<br/><small>(e.g. 15%)</small>"] --> A1
1. Attempt 1 = Original flex (e.g., 15%)
2. Attempt 2 = +3% step (18%)
3. Attempt 3 = +3% step (21%)
4. Attempt 4 = +3% step (24%)
5. … Attempts 5-11 (default) continue adding +3% each time
6. … Additional attempts keep extending the same pattern up to the 12-attempt maximum (up to 51%)
subgraph Attempt1["Attempt 1 — flex 15%"]
A1["Your filters"] -->|not enough| A2["Level = any"]
end
**2 Filter Combinations (per flexibility level):**
A2 -->|not enough| B1
1. Original filters (your configured level filter)
2. Remove level filter (level=any)
subgraph Attempt2["Attempt 2 — flex 18%"]
B1["Your filters"] -->|not enough| B2["Level = any"]
end
**Example progression:**
B2 -->|not enough| C1
subgraph Attempt3["Attempt 3 — flex 21%"]
C1["Your filters"] --> C2["Level = any"]
end
C1 -->|"✅ enough"| Done
A1 -->|"✅ enough"| Done
A2 -->|"✅ enough"| Done
B1 -->|"✅ enough"| Done
B2 -->|"✅ enough"| Done
C2 -->|"✅ / not enough → next …"| Done
Done["✅ Done<br/><small>stops at first success</small>"]
style Start fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style Done fill:#e6fff5,stroke:#00c853,stroke-width:2px
style Attempt1 fill:#f0f9ff,stroke:#00b9e7
style Attempt2 fill:#fff9e6,stroke:#ffb800
style Attempt3 fill:#fff0f0,stroke:#ff8a80
```
Flex 15% + Original filters → Not enough periods
Flex 15% + Level=any → Not enough periods
Flex 18% + Original filters → Not enough periods
Flex 18% + Level=any → SUCCESS! Found 2 periods ✓
(stops here - no need to try more)
```
Each attempt adds +3% flexibility and tries two filter combinations. The system **stops as soon as enough periods are found** — it doesn't keep trying the full matrix.
### Choosing the Number of Attempts

View file

@ -4,8 +4,6 @@ comments: false
# Sensors
> **Note:** This guide is under construction. For now, please refer to the [main README](https://github.com/jpawlowski/hass.tibber_prices/blob/main/README.md) for available sensors.
> **Tip:** Many sensors have dynamic icons and colors! See the **[Dynamic Icons Guide](dynamic-icons.md)** and **[Dynamic Icon Colors Guide](icon-colors.md)** to enhance your dashboards.
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
@ -305,7 +303,466 @@ By following the "Good Example", your automations become simpler, more readable,
## Rating Sensors
Coming soon...
Rating sensors classify prices relative to the **trailing 24-hour average**, answering: "Is the current price cheap, normal, or expensive compared to recent history?"
### How Ratings Work
The integration calculates a **percentage difference** between the current price and the trailing 24-hour average:
```
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 |
**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.
```mermaid
stateDiagram-v2
direction LR
LOW: 🟢 LOW<br/><small>price ≤ 10%</small>
NORMAL: 🟡 NORMAL<br/><small>10% … +10%</small>
HIGH: 🔴 HIGH<br/><small>price ≥ +10%</small>
LOW --> NORMAL: crosses 8%<br/><small>(hysteresis)</small>
NORMAL --> LOW: drops below 10%
NORMAL --> HIGH: rises above +10%
HIGH --> NORMAL: crosses +8%<br/><small>(hysteresis)</small>
```
> **The 2% gap** between entering (10%) and leaving (8%) a state prevents the sensor from flickering back and forth when prices hover near a threshold.
### 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 |
| **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 |
### 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) |
**Which to use?**
- **Automations**: Use **ratings** (3 simple states, configurable thresholds, hysteresis)
- **Dashboards**: Use **levels** (5 color-coded states, more visual granularity)
- **Advanced automations**: Combine both (e.g., "LOW rating AND VERY_CHEAP level")
### 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` |
### Usage in Automations
**Best Practice:** Always use the `rating_level` attribute (lowercase English) instead of the sensor state (which is translated to your HA language):
```yaml
# ✅ Correct — language-independent
condition:
- condition: template
value_template: >
{{ state_attr('sensor.<home_name>_current_price_rating', 'rating_level') == 'low' }}
# ❌ Avoid — breaks when HA language changes
condition:
- condition: state
entity_id: sensor.<home_name>_current_price_rating
state: "Low" # "Niedrig" in German, "Lav" in Norwegian...
```
### Configuration
Rating thresholds can be adjusted in the options flow:
1. Go to **Settings → Devices & Services → Tibber Prices → Configure**
2. Navigate to **Price Rating Thresholds**
3. Adjust LOW/HIGH thresholds, hysteresis, and gap tolerance
See [Configuration](configuration.md#step-3-price-rating-thresholds) for details.
## Level Sensors
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 |
### Available Level Sensors
| 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) |
**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).
## Min/Max Sensors
These sensors show the lowest and highest prices for calendar days and rolling windows:
### Daily Min/Max
| Sensor | Description |
|--------|-------------|
| **Today's Lowest Price** | Minimum price today (00:0023:59) |
| **Today's Highest Price** | Maximum price today (00:0023: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 |
| **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 |
### 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 |
## Timing Sensors
Timing sensors provide **real-time information about Best Price and Peak Price periods**: when they start, end, how long they last, and your progress through them.
```mermaid
stateDiagram-v2
direction LR
IDLE: ⏸️ IDLE<br/><small>No active period</small>
ACTIVE: ▶️ ACTIVE<br/><small>In period</small>
GRACE: ⏳ GRACE<br/><small>60s buffer</small>
IDLE --> ACTIVE: period starts
ACTIVE --> GRACE: period ends
GRACE --> IDLE: 60s elapsed
GRACE --> ACTIVE: new period starts<br/><small>(within grace)</small>
```
**IDLE** = waiting for next period (shows countdown via `next_in_minutes`). **ACTIVE** = inside a period (shows `progress` 0100% and `remaining_minutes`). **GRACE** = short buffer after a period ends, allowing back-to-back periods to merge seamlessly.
### Available Timing Sensors
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** | 0100% 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
**Show countdown to next cheap window:**
```yaml
type: custom:mushroom-entity-card
entity: sensor.<home_name>_best_price_next_in_minutes
name: Next Cheap Window
icon: mdi:clock-fast
```
**Display period progress bar:**
```yaml
type: custom:bar-card
entity: sensor.<home_name>_best_price_progress
name: Best Price Progress
min: 0
max: 100
severity:
- from: 0
to: 50
color: green
- from: 50
to: 80
color: orange
- from: 80
to: 100
color: red
```
**Automation: notify when period is almost over:**
```yaml
automation:
- alias: "Warn: Best Price Ending Soon"
trigger:
- platform: numeric_state
entity_id: sensor.<home_name>_best_price_remaining_minutes
below: 15
condition:
- condition: numeric_state
entity_id: sensor.<home_name>_best_price_remaining_minutes
above: 0
action:
- service: notify.mobile_app
data:
title: "Best Price Ending Soon"
message: "Only {{ states('sensor.<home_name>_best_price_remaining_minutes') }} minutes left!"
```
## Trend Sensors
Trend sensors help you understand **where prices are heading**. They answer the question: "Should I use electricity now, or wait?"
The integration provides two families of trend sensors for different use cases:
### Simple Trend Sensors (1h12h)
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 |
| **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:
```mermaid
stateDiagram-v2
direction LR
SF: ⬇️⬇️ strongly_falling<br/><small>2 · future ≤ 9%</small>
F: ⬇️ falling<br/><small>1 · future ≤ 3%</small>
S: ➡️ stable<br/><small>0 · within ±3%</small>
R: ⬆️ rising<br/><small>+1 · future ≥ +3%</small>
SR: ⬆️⬆️ strongly_rising<br/><small>+2 · future ≥ +9%</small>
SF --> F: price recovers
F --> S: approaches average
S --> R: future rises
R --> SR: accelerates
SR --> R: slows down
R --> S: stabilizes
S --> F: future drops
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 |
**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` |
**Tip:** The `trend_value` attribute (`-2` to `+2`) is ideal for automations — use numeric comparisons instead of matching translated state strings.
### Current Price Trend
**Entity ID:** `sensor.<home_name>_current_price_trend`
This sensor shows the **currently active trend direction** based on a 3-hour future outlook with volatility-adaptive thresholds.
Unlike the simple trend sensors that always compare current price vs future average, the current price trend represents the **ongoing trend** — it remains stable between updates and only changes when the underlying price direction actually shifts.
**States:** Same 5-level scale as simple trends.
**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` |
### Next Price Trend Change
**Entity ID:** `sensor.<home_name>_next_price_trend_change`
This sensor predicts **when the current trend will change** by scanning future intervals. It requires 3 consecutive intervals (configurable: 26) confirming the new trend before reporting a change (hysteresis), which prevents false alarms from short-lived price spikes.
**Important:** Only **direction changes** count as trend changes. The five states are grouped into three directions:
| Direction | States |
|-----------|--------|
| **falling** | `strongly_falling`, `falling` |
| **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`.
**State:** Timestamp of the next trend change (or unavailable if no change predicted).
**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` |
### How to Use Trend Sensors for Decisions
:::danger Common Misconception — Don't "Wait for Stable"!
A natural intuition is to treat trend states like a stock ticker:
- ❌ "It's **falling** → I'll wait until it reaches **stable** (the bottom)"
- ❌ "It's **rising** → too late, I missed the best price"
- ❌ "It's **stable** → now is the perfect time to act!"
**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 |
**"Rising" is NOT "too late" — it means NOW is the best time because prices will be higher later.**
:::
#### Basic Automation Pattern
For most appliances (dishwasher, washing machine, dryer), a single trend sensor is enough:
```yaml
# Example: Start dishwasher when prices are favorable
trigger:
- 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
action:
- 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 |
#### Dashboard Quick-Glance
On your dashboard, trend sensors give an instant overview:
- 🟢 All **falling/strongly_falling** → "Relax, prices are dropping — wait"
- 🔴 All **rising/strongly_rising** → "Start everything you can — it only gets more expensive"
- 🟡 **Mixed** → Compare short-term vs. long-term sensors, or check the Best Price Period sensor
### Trend Sensors vs Average Sensors
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 |
**Design principle:** Use **trend sensors** (enum) for visual feedback at a glance, use **average sensors** (numeric) for precise decision-making in automations.
### Configuration
Trend thresholds can be adjusted in the options flow:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** on your home
3. Navigate to **📈 Price Trend Thresholds**
4. Adjust the rising/falling and strongly rising/falling percentages
The thresholds are **volatility-adaptive**: on days with high price volatility, thresholds are widened automatically to prevent constant state changes. This means the trend sensors give more stable readings during volatile market conditions.
## Diagnostic Sensors

View file

@ -4,15 +4,138 @@ comments: false
# Troubleshooting
> **Note:** This guide is under construction.
## Common Issues
Coming soon...
### Sensors Show "Unavailable"
**After initial setup or HA restart:**
This is normal. The integration needs up to one update cycle (15 minutes) to fetch data from the Tibber API. If sensors remain unavailable after 30 minutes:
1. Check your internet connection
2. Verify your Tibber API token is still valid at [developer.tibber.com](https://developer.tibber.com)
3. Check the logs for error messages (see [Debug Logging](#debug-logging) below)
**After working fine previously:**
- **API communication error**: Tibber's API may be temporarily down. The integration retries automatically — wait 1530 minutes.
- **Authentication expired**: If you see a "Reauth required" notification in HA, your API token needs to be re-entered. Go to **Settings → Devices & Services → Tibber Prices** and follow the reauth flow.
- **Rate limiting**: If you have multiple integrations using the same Tibber token, you may hit API rate limits. Check logs for "429" or "rate limit" messages.
### Tomorrow's Prices Not Available
Tomorrow's electricity prices are typically published by Tibber between **13:00 and 15:00 CET** (Central European Time). Before that time, all "tomorrow" sensors will show unavailable or their last known state.
The integration automatically polls more frequently in the afternoon to detect when tomorrow's data becomes available. No manual action is needed.
### Wrong Currency or Price Units
If prices show in the wrong currency or wrong unit (EUR vs ct):
1. Go to **Settings → Devices & Services → Tibber Prices → Configure**
2. Check the **Currency Display** step
3. Choose between base units (EUR, NOK, SEK) and sub-units (ct, øre)
Note: The currency is determined by your Tibber account's home country and cannot be changed — only the display unit (base vs. sub-unit) is configurable.
### No Best/Peak Price Periods Found
If the Best Price Period or Peak Price Period binary sensors never turn on:
1. **Check your flex settings**: A flex value that's too low may filter out all intervals. Try increasing it (e.g., from 10% to 20%).
2. **Enable relaxation**: In the options flow, enable relaxation for the affected period type. This automatically increases flex until periods are found.
3. **Check daily price variation**: On days with very flat prices (low volatility), periods may not meet the threshold criteria. This is expected behavior — the integration correctly identifies that no intervals stand out.
See the [Period Calculation Guide](period-calculation.md) for detailed configuration advice.
### Entities Duplicated After Reconfiguration
If you see duplicate entities after changing settings:
1. Go to **Settings → Devices & Services → Entities**
2. Filter by "Tibber Prices"
3. Remove any disabled or orphaned entities
4. Restart Home Assistant
### Integration Not Showing After Installation
If the integration doesn't appear in **Settings → Devices & Services → Add Integration**:
1. Confirm you restarted Home Assistant after installing via HACS
2. Clear your browser cache (Ctrl+Shift+R)
3. Check the HA logs for import errors related to `tibber_prices`
## Debug Logging
Coming soon...
When reporting issues, debug logs help identify the problem quickly.
### Enable Debug Logging
Add this to your `configuration.yaml`:
```yaml
logger:
default: warning
logs:
custom_components.tibber_prices: debug
```
Restart Home Assistant for the change to take effect.
### Targeted Logging
For specific subsystems, you can enable logging selectively:
```yaml
logger:
default: warning
logs:
# API communication (requests, responses, errors)
custom_components.tibber_prices.api: debug
# Coordinator (data updates, caching, scheduling)
custom_components.tibber_prices.coordinator: debug
# Period calculation (best/peak price detection)
custom_components.tibber_prices.coordinator.period_handlers: debug
# Sensor value calculation
custom_components.tibber_prices.sensor: debug
```
### Temporary Debug Logging (No Restart)
You can also enable debug logging temporarily from the HA UI:
1. Go to **Developer Tools → Services**
2. Call service: `logger.set_level`
3. Data:
```yaml
custom_components.tibber_prices: debug
```
This resets when HA restarts.
### Downloading Diagnostics
For bug reports, include the integration's diagnostic dump:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click the three-dot menu (⋮) on the integration card
3. Select **Download diagnostics**
The downloaded file includes configuration, cache status, period information, and recent errors — with sensitive data redacted.
### What to Include in Bug Reports
When opening a [GitHub issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new):
1. **Integration version** (from Settings → Devices & Services → Tibber Prices)
2. **Home Assistant version** (from Settings → About)
3. **Description** of the problem and expected behavior
4. **Debug logs** (relevant excerpts from the HA log)
5. **Diagnostics file** (downloaded as described above)
6. **Steps to reproduce** (if applicable)
## Getting Help

41853
docs/user/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,50 +1,51 @@
{
"name": "docs-split-user",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.9.2",
"@docusaurus/preset-classic": "3.9.2",
"@docusaurus/theme-mermaid": "^3.9.2",
"@giscus/react": "^3.1.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.6.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.9.2",
"typescript": "~5.6.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=20.0"
}
"name": "docs-split-user",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^3.10.0",
"@docusaurus/faster": "^3.10.0",
"@docusaurus/preset-classic": "^3.10.0",
"@docusaurus/theme-mermaid": "^3.10.0",
"@giscus/react": "^3.1.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.6.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.0",
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/types": "^3.10.0",
"typescript": "~6.0.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=20.0"
}
}

View file

@ -11,8 +11,8 @@ export default function GiscusComponent() {
repoId="R_kgDOObwUag"
category="General"
categoryId="DIC_kwDOObwUas4CzVw_"
mapping="pathname"
strict="0"
mapping="og:title"
strict="1"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="top"

View file

@ -226,3 +226,176 @@ h1, h2, h3, h4, h5, h6 {
[data-theme='dark'] .bmc-logo-dark {
display: block;
}
/* ── Mermaid diagram zoom / lightbox ──────────────────────────── */
/* Wrapper — entire diagram is the click target */
.mermaid-zoom-wrapper {
position: relative;
display: block;
cursor: zoom-in;
border-radius: var(--ifm-card-border-radius, 0.5rem);
/* Clip the hover overlay to the same rounded corners */
overflow: hidden;
/* Solid background so the SVG is never transparent against the page */
background: var(--ifm-background-color);
}
/* Ensure the SVG itself also has a solid background (covers Mermaid inline styles) */
.mermaid-zoom-wrapper svg {
display: block;
background: var(--ifm-background-color);
border-radius: inherit;
}
/* Semi-transparent tint that fades in on hover */
.mermaid-zoom-hint {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
opacity: 0;
background: rgba(0, 0, 0, 0);
transition: opacity 0.2s ease, background 0.2s ease;
z-index: 1;
}
.mermaid-zoom-wrapper:hover .mermaid-zoom-hint,
.mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint {
opacity: 1;
background: rgba(0, 0, 0, 0.06);
}
[data-theme='dark'] .mermaid-zoom-wrapper:hover .mermaid-zoom-hint,
[data-theme='dark'] .mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint {
background: rgba(255, 255, 255, 0.06);
}
/* Centered pill badge inside the tint */
.mermaid-zoom-hint-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.8rem;
background: var(--ifm-background-surface-color);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 2rem;
color: var(--ifm-color-emphasis-800);
font-size: 0.78rem;
font-weight: 600;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
transform: translateY(4px);
transition: transform 0.2s ease;
}
.mermaid-zoom-wrapper:hover .mermaid-zoom-hint-badge,
.mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint-badge {
transform: translateY(0);
}
/* Focus ring for keyboard users */
.mermaid-zoom-wrapper:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
}
/* Touch devices: no hover state — show badge permanently so users know it's tappable */
@media (hover: none) {
.mermaid-zoom-hint {
opacity: 1;
background: transparent;
}
.mermaid-zoom-hint-badge {
transform: translateY(0);
/* Slightly more opaque so it reads well without hover context */
opacity: 0.85;
}
}
/* Full-screen backdrop */
.mermaid-lightbox {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
cursor: zoom-out;
animation: mermaid-fadein 0.15s ease;
}
@keyframes mermaid-fadein {
from { opacity: 0; }
to { opacity: 1; }
}
/* The white/dark card containing the diagram */
.mermaid-lightbox-inner {
position: relative;
width: min(90vw, 1200px);
max-height: 85vh;
overflow: auto;
padding: 1.5rem;
background: var(--ifm-background-color);
border-radius: var(--ifm-card-border-radius);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4);
cursor: default;
animation: mermaid-scalein 0.15s ease;
}
@keyframes mermaid-scalein {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.mermaid-lightbox-inner > svg {
display: block;
width: 100%;
height: auto;
/* !important: overrides any inline background Mermaid bakes into the SVG */
background: var(--ifm-background-color) !important;
border-radius: calc(var(--ifm-card-border-radius, 0.5rem) - 2px);
}
/*
* Override Mermaid's hardcoded background rects.
* Mermaid uses several class names depending on diagram type and version:
* .background sequence diagrams
* .bb class/state diagrams
* The first <rect> child is used as a background fill by all diagram types.
*/
.mermaid-lightbox-inner svg .background,
.mermaid-lightbox-inner svg .bb,
.mermaid-lightbox-inner svg > rect:first-child {
fill: var(--ifm-background-color) !important;
}
/* Close button (top-right corner of the card) */
.mermaid-lightbox-close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
padding: 0;
background: var(--ifm-background-surface-color);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 0.4rem;
color: var(--ifm-color-emphasis-700);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.mermaid-lightbox-close:hover {
background: var(--ifm-color-danger);
border-color: var(--ifm-color-danger);
color: #fff;
}

View file

@ -14,6 +14,21 @@ export default function DocItemFooterWrapper(props) {
<DocItemFooter {...props} />
{enableComments && (
<div style={{ marginTop: '3rem' }}>
<p style={{
fontSize: '0.85rem',
color: 'var(--ifm-color-emphasis-600)',
marginBottom: '0.75rem',
}}>
💬 <strong>Comments are page-specific.</strong> For a new question or idea,{' '}
<a
href="https://github.com/jpawlowski/hass.tibber_prices/discussions/new/choose"
target="_blank"
rel="noopener noreferrer"
>
open a dedicated Discussion on GitHub
</a>{' '}
so it gets its own thread and proper visibility.
</p>
<GiscusComponent />
</div>
)}

View file

@ -0,0 +1,167 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
import OriginalMermaid from '@theme-original/Mermaid';
import type MermaidType from '@theme/Mermaid';
import type { WrapperProps } from '@docusaurus/types';
type Props = WrapperProps<typeof MermaidType>;
export default function MermaidWrapper(props: Props): React.JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
const lightboxRef = useRef<HTMLDivElement>(null);
const [overlayOpen, setOverlayOpen] = useState(false);
const [svgMarkup, setSvgMarkup] = useState('');
const [mounted, setMounted] = useState(false);
// Only run portals client-side (SSR safety)
useEffect(() => {
setMounted(true);
}, []);
const handleOpen = useCallback(() => {
const svg = containerRef.current?.querySelector('svg');
if (!svg) return;
// Clone the SVG and strip fixed dimensions so it scales freely
const clone = svg.cloneNode(true) as SVGElement;
clone.removeAttribute('width');
clone.removeAttribute('height');
// Bake in the current page background so the SVG is never transparent,
// even if Mermaid's internal <style> or <rect> elements override CSS.
const bg =
getComputedStyle(document.documentElement)
.getPropertyValue('--ifm-background-color')
.trim() || '#ffffff';
clone.style.cssText = `width:100%;height:auto;display:block;background:${bg};`;
setSvgMarkup(clone.outerHTML);
setOverlayOpen(true);
}, []);
const handleClose = useCallback(() => {
setOverlayOpen(false);
}, []);
// Keyboard + body-scroll lock + focus trap while overlay is open
useEffect(() => {
if (!overlayOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleClose();
return;
}
// Focus trap: keep Tab/Shift+Tab inside the lightbox
if (e.key === 'Tab' && lightboxRef.current) {
const focusable = lightboxRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last?.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first?.focus();
}
}
}
};
document.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
// Move focus into the lightbox on open
const closeBtn = lightboxRef.current?.querySelector<HTMLElement>('button');
closeBtn?.focus();
return () => {
document.removeEventListener('keydown', onKey);
document.body.style.overflow = '';
};
}, [overlayOpen, handleClose]);
return (
<div
ref={containerRef}
className="mermaid-zoom-wrapper"
onClick={handleOpen}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleOpen();
}
}}
aria-label="View diagram enlarged"
>
<OriginalMermaid {...props} />
{/* Hover hint — pointer-events:none so it never swallows clicks */}
<div className="mermaid-zoom-hint" aria-hidden="true">
<div className="mermaid-zoom-hint-badge">
<svg
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
<line x1="11" y1="8" x2="11" y2="14" />
<line x1="8" y1="11" x2="14" y2="11" />
</svg>
Enlarge
</div>
</div>
{mounted &&
overlayOpen &&
createPortal(
<div
ref={lightboxRef}
className="mermaid-lightbox"
role="dialog"
aria-modal="true"
aria-label="Enlarged diagram"
onClick={(e) => { e.stopPropagation(); handleClose(); }}
>
<div
className="mermaid-lightbox-inner"
onClick={(e) => e.stopPropagation()}
// Safe: SVG content is cloned from our own rendered DOM node
dangerouslySetInnerHTML={{ __html: svgMarkup }}
/>
<button
className="mermaid-lightbox-close"
onClick={(e) => { e.stopPropagation(); handleClose(); }}
aria-label="Close enlarged view"
title="Close (Esc)"
>
<svg
viewBox="0 0 24 24"
width="18"
height="18"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
aria-hidden="true"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>,
document.body,
)}
</div>
);
}

View file

@ -56,4 +56,4 @@ This is an independent, community-maintained custom integration. It is **not** a
---
**Note:** These guides are for end users. If you want to contribute to development, see the [Developer Documentation](/hass.tibber_prices/developer/).
**Note:** These guides are for end users. If you want to contribute to development, see the [Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/).

View file

@ -0,0 +1,288 @@
# Actions (Services)
Home Assistant now surfaces these backend service endpoints as **Actions** in the UI (for example, Developer Tools → Actions or the Action editor inside dashboards). Behind the scenes they are still Home Assistant services that use the `service:` key, but this guide uses the word “action” whenever we refer to the user interface.
You can still call them from automations, scripts, and dashboards the same way as before (`service: tibber_prices.get_chartdata`, etc.), just remember that the frontend officially lists them as actions.
## Finding Your Entry ID
Every action requires an `entry_id` parameter that tells Home Assistant which Tibber home (integration instance) to use. If you only have one home, there is still exactly one entry ID — you just need to know where to find it.
### In the Action UI — no lookup needed
When you use the action through the Home Assistant interface (Developer Tools → Actions, or the Action picker inside the automation / script editor), the `entry_id` field renders as a **dropdown list** showing all your configured Tibber Prices instances. Just select your home from the drop-down and Home Assistant fills in the correct ID automatically. You never have to deal with the raw ID string.
### In YAML — copy from the integration menu
When you write YAML directly (automations, scripts, Lovelace dashboard cards), you need the actual ID string. The quickest way to get it:
1. Go to **Settings → Devices & Services**
2. Find the **Tibber Prices** integration card
3. Click the **⋮** (three-dot) menu on the card
4. Choose **"Copy Config Entry ID"**
5. Paste the value wherever you see `YOUR_ENTRY_ID` in the YAML examples
The ID looks like a long alphanumeric string, for example `01JKPC7AB3EF4GH5IJ6KL7MN8P`.
:::tip Multiple homes?
If you have configured more than one Tibber home, each home has its own entry ID. Repeat the steps above for each integration card to get the individual IDs.
:::
## Available Actions
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
### tibber_prices.get_chartdata
**Purpose:** Returns electricity price data in chart-friendly formats for visualization and analysis.
**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)
**Basic Example:**
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_ENTRY_ID
day: ["today", "tomorrow"]
output_format: array_of_objects
response_variable: chart_data
```
**Response Format:**
```json
{
"data": [
{
"start_time": "2025-11-17T00:00:00+01:00",
"price_per_kwh": 0.2534
},
{
"start_time": "2025-11-17T00:15:00+01:00",
"price_per_kwh": 0.2498
}
]
}
```
**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) |
**Rolling Window Mode:**
Omit the `day` parameter to get a dynamic 48-hour rolling window that automatically adapts to data availability:
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_ENTRY_ID
# Omit 'day' for rolling window
output_format: array_of_objects
response_variable: chart_data
```
**Behavior:**
- **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.
**Period Filter Example:**
Get best price periods as summaries instead of intervals:
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_ENTRY_ID
period_filter: best_price # or peak_price
day: ["today", "tomorrow"]
include_level: true
include_rating_level: true
response_variable: periods
```
**Advanced Filtering:**
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_ENTRY_ID
level_filter: ["VERY_CHEAP", "CHEAP"] # Only cheap periods
rating_level_filter: ["LOW"] # Only low-rated prices
insert_nulls: segments # Add nulls at segment boundaries
```
**Complete Documentation:**
For detailed parameter descriptions, open **Developer Tools → Actions** (the UI label) and select `tibber_prices.get_chartdata`. The inline documentation is still stored in `services.yaml` because actions are backed by services.
---
### tibber_prices.get_apexcharts_yaml
> ⚠️ **IMPORTANT:** This action generates a **basic example configuration** as a starting point, NOT a complete solution for all ApexCharts features.
>
> This integration is primarily a **data provider**. The generated YAML demonstrates how to use the `get_chartdata` action to fetch price data. Due to the segmented nature of our data (different time periods per series) and the use of Home Assistant's service API instead of entity attributes, many advanced ApexCharts features (like `in_header`, certain transformations) are **not compatible** or require manual customization.
>
> **You are welcome to customize** the generated YAML for your specific needs, but comprehensive ApexCharts configuration support is beyond the scope of this integration. Community contributions with improved configurations are always appreciated!
>
> **For custom solutions:** Use the `get_chartdata` action directly to build your own charts with full control over the data format and visualization.
**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)
**✨ Key Features:**
- **Automatic Color-Coded Series**: Separate series for each price level (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) or rating (LOW, NORMAL, HIGH)
- **Dynamic Y-Axis Scaling**: Rolling window modes automatically use `chart_metadata` sensor for optimal Y-axis bounds
- **Best Price Period Highlights**: Optional vertical bands showing detected best price periods
- **Translated Labels**: Automatically uses your Home Assistant language setting
- **Clean Gap Visualization**: Proper NULL insertion for missing data segments
**Quick Example:**
```yaml
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
response_variable: apexcharts_config
```
**Day Parameter Options:**
- **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
- **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
**Dynamic Y-Axis Scaling (Rolling Window Modes):**
Rolling window configurations automatically integrate with the `chart_metadata` sensor for optimal chart appearance:
- **Automatic bounds**: Y-axis min/max adjust to data range
- **No manual configuration**: Works out of the box if sensor is enabled
- **Fallback behavior**: If sensor is disabled, uses ApexCharts auto-scaling
- **Real-time updates**: Y-axis adapts when price data changes
**Example: Today's Prices (Static View)**
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_ENTRY_ID
day: today
level_type: rating_level
response_variable: config
# Use in dashboard:
type: custom:apexcharts-card
# ... paste generated config
```
**Example: Rolling 48h Window (Dynamic View)**
```yaml
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
highlight_best_price: true
response_variable: config
# Use in dashboard:
type: custom:config-template-card
entities:
- binary_sensor.<home_name>_tomorrow_s_data_available
- sensor.<home_name>_chart_metadata # For dynamic Y-axis
card:
# ... paste generated config
```
**Screenshots:**
_Screenshots coming soon for all 4 modes: today, tomorrow, rolling_window, rolling_window_autozoom_
**Level Type Options:**
- **`rating_level`** (default): 3 series (LOW, NORMAL, HIGH) - based on your personal thresholds
- **`level`**: 5 series (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) - absolute price ranges
**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
**Important Notes:**
- **Config Template Card** is only required for rolling window modes (enables dynamic Y-axis)
- Fixed day views (`today`, `tomorrow`, `yesterday`) work with ApexCharts Card alone
- Generated YAML is a starting point - customize colors, styling, features as needed
- All labels are automatically translated to your Home Assistant language
Use the response in Lovelace dashboards by copying the generated YAML.
**Documentation:** Refer to **Developer Tools → Actions** for descriptions of the fields exposed by this action.
---
### tibber_prices.refresh_user_data
**Purpose:** Forces an immediate refresh of user data (homes, subscriptions) from the Tibber API.
**Example:**
```yaml
service: tibber_prices.refresh_user_data
data:
entry_id: YOUR_ENTRY_ID
```
**Note:** User data is cached for 24 hours. Trigger this action only when you need immediate updates (e.g., after changing Tibber subscriptions).
---
## Migration from Chart Data Export Sensor
If you're still using the `sensor.<home_name>_chart_data_export` sensor, consider migrating to the `tibber_prices.get_chartdata` action:
**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)
**Migration Steps:**
1. Note your current sensor configuration (Step 7 in Options Flow)
2. Create automation/script that calls `tibber_prices.get_chartdata` with the same parameters
3. Test the new approach
4. Disable the old sensor when satisfied

View file

@ -0,0 +1,553 @@
# Automation Examples
> **Tip:** For dashboard examples with dynamic icons and colors, see the **[Dynamic Icons Guide](dynamic-icons.md)** and **[Dynamic Icon Colors Guide](icon-colors.md)**.
## 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)
---
> **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.<home_name>_...`, `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).
>
> These examples provide a good starting point but must be tailored to your individual Home Assistant setup.
>
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
## Price-Based Automations
### Understanding V-Shaped Price Days
Some days have a **V-shaped** or **U-shaped** price curve: prices drop to very cheap levels (often rated VERY_CHEAP) for an extended period, then rise again. This is common during sunny midday hours (solar surplus) or low-demand nights.
**The challenge:** The Best Price Period might only cover 12 hours (the absolute cheapest window), but prices could remain favorable for 46 hours. If you only rely on the Best Price Period binary sensor, you miss out on the surrounding cheap hours.
**The solution:** Combine multiple sensors to ride the full cheap wave:
```mermaid
gantt
title V-Shaped Price Day — Best Price vs Full Cheap Window
dateFormat HH:mm
axisFormat %H:%M
section Price Level
NORMAL :done, n1, 06:00, 2h
CHEAP :active, c1, 08:00, 2h
VERY CHEAP :crit, vc, 10:00, 2h
CHEAP :active, c2, 12:00, 2h
NORMAL :done, n2, 14:00, 2h
EXPENSIVE : e1, 16:00, 2h
section Detected
Best Price Period :crit, bp, 10:00, 2h
section Your Goal
Run while cheap + trend OK :active, goal, 08:00, 6h
```
> **Key insight:** The Best Price Period covers only the absolute minimum (2h). By combining the period sensor with price level and trend, you can extend device runtime to the full 6h cheap window.
### Use Case: Ride the Full Cheap Wave
This automation starts a flexible load when the best price period begins, but keeps it running as long as prices remain favorable — even after the period ends.
```yaml
automation:
- alias: "Heat Pump - Extended Cheap Period"
description: "Run heat pump during the full cheap price window, not just best-price period"
mode: restart
trigger:
# Start: Best price period begins
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
id: best_price_on
# Re-evaluate: Every 15 minutes while running
- platform: state
entity_id: sensor.<home_name>_current_electricity_price
id: price_update
condition:
# Continue running while EITHER condition is true:
- condition: or
conditions:
# Path 1: We're in a best price period
- condition: state
entity_id: binary_sensor.<home_name>_best_price_period
state: "on"
# Path 2: Price is still cheap AND trend is not rising
- condition: and
conditions:
- condition: template
value_template: >
{% set level = state_attr('sensor.<home_name>_current_electricity_price', 'rating_level') %}
{{ level in ['VERY_CHEAP', 'CHEAP'] }}
- condition: template
value_template: >
{% set trend = state_attr('sensor.<home_name>_current_price_trend', 'trend_value') | int(0) %}
{{ trend <= 0 }}
action:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 22
```
**How it works:**
1. Starts when the best price period triggers
2. On each price update, rechecks conditions
3. Keeps running while prices are CHEAP or VERY_CHEAP **and** the trend is not rising
4. Stops when either prices climb above CHEAP or the trend turns to rising
### Use Case: Pre-Emptive Start Before Best Price
Use the trend to start slightly before the cheapest period — useful for appliances with warm-up time:
```yaml
automation:
- alias: "Water Heater - Pre-Heat Before Cheapest"
trigger:
- platform: state
entity_id: sensor.<home_name>_current_electricity_price
condition:
# Conditions: Prices are falling AND we're approaching cheap levels
- condition: template
value_template: >
{% set trend_value = state_attr('sensor.<home_name>_price_trend_3h', 'trend_value') | int(0) %}
{% set level = state_attr('sensor.<home_name>_current_electricity_price', 'rating_level') %}
{{ trend_value <= -1 and level in ['CHEAP', 'NORMAL'] }}
# AND: The next 3 hours will be cheaper on average
- condition: template
value_template: >
{% set current = states('sensor.<home_name>_current_electricity_price') | float %}
{% set future_avg = state_attr('sensor.<home_name>_price_trend_3h', 'next_3h_avg') | float %}
{{ future_avg < current }}
action:
- service: water_heater.set_temperature
target:
entity_id: water_heater.boiler
data:
temperature: 60
```
### Use Case: Protect Against Rising Prices
Stop or reduce consumption when prices are climbing:
```yaml
automation:
- alias: "EV Charger - Stop When Prices Rising"
trigger:
- platform: template
value_template: >
{{ state_attr('sensor.<home_name>_current_price_trend', 'trend_value') | int(0) >= 1 }}
condition:
# Only stop if price is already above typical level
- condition: template
value_template: >
{% set level = state_attr('sensor.<home_name>_current_electricity_price', 'rating_level') %}
{{ level in ['NORMAL', 'EXPENSIVE', 'VERY_EXPENSIVE'] }}
action:
- service: switch.turn_off
target:
entity_id: switch.ev_charger
- service: notify.mobile_app
data:
message: >
EV charging paused — prices are {{ states('sensor.<home_name>_current_price_trend') }}
and currently at {{ states('sensor.<home_name>_current_electricity_price') }}
{{ state_attr('sensor.<home_name>_current_electricity_price', 'unit_of_measurement') }}.
Next trend change in ~{{ state_attr('sensor.<home_name>_next_price_trend_change', 'minutes_until_change') }} minutes.
```
### Use Case: Multi-Window Trend Strategy for Flexible Loads
Combine short-term and long-term trend sensors for smarter decisions. This example manages a heat pump boost:
- If **both** windows say `rising` → prices only go up from here, boost now
- If short-term is `falling` but long-term is `rising` → brief dip coming, wait for it then boost
- If **both** say `falling` → prices are dropping, definitely wait
- If long-term says `falling` → cheaper hours ahead, no rush
```yaml
automation:
- alias: "Heat Pump - Smart Boost Using Multi-Window Trends"
description: >
Combines 1h (short-term) and 6h (long-term) trend windows.
Rising = current price is LOWER than future average = act now.
Falling = current price is HIGHER than future average = wait.
trigger:
- platform: state
entity_id: sensor.<home_name>_price_trend_1h
- platform: state
entity_id: sensor.<home_name>_price_trend_6h
condition:
# Only consider if best price period is NOT active
# (if it IS active, a separate automation handles it)
- condition: state
entity_id: binary_sensor.<home_name>_best_price_period
state: "off"
action:
- choose:
# Case 1: Both rising → prices only go up, boost NOW
- conditions:
- condition: template
value_template: >
{% set t1 = state_attr('sensor.<home_name>_price_trend_1h', 'trend_value') | int(0) %}
{% set t6 = state_attr('sensor.<home_name>_price_trend_6h', 'trend_value') | int(0) %}
{{ t1 >= 1 and t6 >= 1 }}
sequence:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 22
# Case 2: 1h falling + 6h rising → brief dip, wait then act
- conditions:
- condition: template
value_template: >
{% set t1 = state_attr('sensor.<home_name>_price_trend_1h', 'trend_value') | int(0) %}
{% set t6 = state_attr('sensor.<home_name>_price_trend_6h', 'trend_value') | int(0) %}
{{ t1 <= -1 and t6 >= 1 }}
sequence:
# Short-term dip — wait for it to bottom out
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 20
# Case 3: 6h falling → cheaper hours ahead, reduce now
- conditions:
- condition: template
value_template: >
{% set t6 = state_attr('sensor.<home_name>_price_trend_6h', 'trend_value') | int(0) %}
{{ t6 <= -1 }}
sequence:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 19
# Default: stable on both → maintain normal operation
default:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 20.5
```
:::tip Why "rising" means "act now"
A common misconception: **"rising" does NOT mean "too late"**. It means your current price is **lower** than the future average — so right now is actually a good time. See [How to Use Trend Sensors for Decisions](sensors.md#how-to-use-trend-sensors-for-decisions) in the sensor documentation for details.
:::
### 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` |
---
## Volatility-Aware Automations
These examples show how to create robust automations that only act when price differences are meaningful, avoiding unnecessary actions on days with flat prices.
### Use Case: Only Act on Meaningful Price Variations
On days with low price variation, the difference between "cheap" and "expensive" periods can be just a fraction of a cent. This automation charges a home battery only when the volatility is high enough to result in actual savings.
**Best Practice:** Instead of checking a numeric percentage, this automation checks the sensor's classified state. This makes the automation simpler and respects the volatility thresholds you have configured centrally in the integration's options.
```yaml
automation:
- alias: "Home Battery - Charge During Best Price (Moderate+ Volatility)"
description: "Charge home battery during Best Price periods, but only on days with meaningful price differences"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
# Best Practice: Check the classified volatility level.
# This ensures the automation respects the thresholds you set in the config options.
# We use the 'price_volatility' attribute for a language-independent check.
# 'low' means minimal savings, so we only run if it's NOT low.
- condition: template
value_template: >
{{ state_attr('sensor.<home_name>_today_s_price_volatility', 'price_volatility') != 'low' }}
# Only charge if battery has capacity
- condition: numeric_state
entity_id: sensor.home_battery_level
below: 90
action:
- service: switch.turn_on
target:
entity_id: switch.home_battery_charge
- service: notify.mobile_app
data:
message: >
Home battery charging started. Price: {{ states('sensor.<home_name>_current_electricity_price') }} {{ state_attr('sensor.<home_name>_current_electricity_price', 'unit_of_measurement') }}.
Today's volatility is {{ state_attr('sensor.<home_name>_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.
### Use Case: Combined Volatility and Absolute Price Check
This is the most robust approach. It trusts the "Best Price" classification on volatile days but adds a backup absolute price check for low-volatility days. This handles situations where prices are globally low, even if the daily variation is minimal.
```yaml
automation:
- alias: "EV Charging - Smart Strategy"
description: "Charge EV using volatility-aware logic"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
# Check battery level
- condition: numeric_state
entity_id: sensor.ev_battery_level
below: 80
# Strategy: Moderate+ volatility OR the price is genuinely cheap
- condition: or
conditions:
# Path 1: Volatility is not 'low', so we trust the 'Best Price' period classification.
- condition: template
value_template: >
{{ state_attr('sensor.<home_name>_today_s_price_volatility', 'price_volatility') != 'low' }}
# Path 2: Volatility is low, but we charge anyway if the price is below an absolute cheapness threshold.
- condition: numeric_state
entity_id: sensor.<home_name>_current_electricity_price
below: 0.18
action:
- service: switch.turn_on
target:
entity_id: switch.ev_charger
- service: notify.mobile_app
data:
message: >
EV charging started. Price: {{ states('sensor.<home_name>_current_electricity_price') }} {{ state_attr('sensor.<home_name>_current_electricity_price', 'unit_of_measurement') }}.
Today's volatility is {{ state_attr('sensor.<home_name>_today_s_price_volatility', 'price_volatility') }}.
```
**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.
### Use Case: Using the Period's Own Volatility Attribute
For maximum simplicity, you can use the attributes of the `best_price_period` sensor itself. It contains the volatility classification for the day the period belongs to. This is especially useful for periods that span across midnight.
```yaml
automation:
- alias: "Heat Pump - Smart Heating Using Period's Volatility"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
# Best Practice: Check if the period's own volatility attribute is not 'low'.
# This correctly handles periods that start today but end tomorrow.
- condition: template
value_template: >
{{ state_attr('binary_sensor.<home_name>_best_price_period', 'volatility') != 'low' }}
action:
- service: climate.set_temperature
target:
entity_id: climate.heat_pump
data:
temperature: 22 # Boost temperature during cheap period
```
**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.
---
## Best Hour Detection
### Use Case: Find the Best Time to Run an Appliance
Use future average sensors to determine the cheapest upcoming window for a timed appliance (e.g., dishwasher with 2-hour ECO program):
```yaml
automation:
- alias: "Dishwasher - Schedule for Cheapest 2h Window"
trigger:
# Check when tomorrow's data arrives (typically 13:00-14:00)
- platform: state
entity_id: sensor.<home_name>_price_tomorrow
attribute: price_mean
condition:
# Only if tomorrow data is available
- condition: template
value_template: >
{{ state_attr('sensor.<home_name>_price_tomorrow', 'price_mean') is not none }}
action:
# Compare different future windows to find cheapest start
- variables:
next_2h: "{{ state_attr('sensor.<home_name>_price_trend_2h', 'next_2h_avg') | float(999) }}"
next_4h: "{{ state_attr('sensor.<home_name>_price_trend_4h', 'next_4h_avg') | float(999) }}"
daily_avg: "{{ state_attr('sensor.<home_name>_price_today', 'price_median') | float(999) }}"
- service: notify.mobile_app
data:
title: "Dishwasher Scheduling"
message: >
Next 2h avg: {{ next_2h }} ct/kWh
Next 4h avg: {{ next_4h }} ct/kWh
Today's typical: {{ daily_avg }} ct/kWh
{% if next_2h < daily_avg * 0.8 %}
→ Now is a great time to start!
{% else %}
→ Consider waiting for a cheaper window.
{% endif %}
```
### Use Case: Notify When Cheapest Window Starts
Get a push notification when the best price period begins:
```yaml
automation:
- alias: "Notify - Cheap Window Started"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
action:
- service: notify.mobile_app
data:
title: "⚡ Cheap Electricity Now!"
message: >
Best price period started.
Current price: {{ states('sensor.<home_name>_current_electricity_price') }}
{{ state_attr('sensor.<home_name>_current_electricity_price', 'unit_of_measurement') }}.
Duration: {{ state_attr('binary_sensor.<home_name>_best_price_period', 'duration_minutes') }} minutes.
Average period price: {{ state_attr('binary_sensor.<home_name>_best_price_period', 'price_mean') }}
{{ state_attr('sensor.<home_name>_current_electricity_price', 'unit_of_measurement') }}.
```
---
## ApexCharts Cards
> ⚠️ **IMPORTANT:** The `tibber_prices.get_apexcharts_yaml` service generates a **basic example configuration** as a starting point. It is NOT a complete solution for all ApexCharts features.
>
> This integration is primarily a **data provider**. Due to technical limitations (segmented time periods, service API usage), many advanced ApexCharts features require manual customization or may not be compatible.
>
> **For advanced customization:** Use the `get_chartdata` service directly to build charts tailored to your specific needs. Community contributions with improved configurations are welcome!
The `tibber_prices.get_apexcharts_yaml` service generates basic ApexCharts card configuration examples for visualizing electricity prices.
:::info Finding your Entry ID (`entry_id`)
The examples below contain `entry_id: YOUR_ENTRY_ID`. This value identifies which Tibber home (integration instance) the action targets.
**In the Action UI (Developer Tools → Actions or the automation editor):** The `entry_id` field is a **dropdown** — just select your Tibber home and HA fills in the correct ID automatically.
**In YAML:** Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** (three-dot) menu, and choose **"Copy Config Entry ID"**. Paste the copied value in place of `YOUR_ENTRY_ID`.
:::
### 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
1. Open HACS → Frontend
2. Search for "ApexCharts Card" and install
3. (Optional) Search for "Config Template Card" and install if you want rolling window mode
### Example: Fixed Day View
```yaml
# Generate configuration via automation/script
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
response_variable: apexcharts_config
```
Then copy the generated YAML into your Lovelace dashboard.
### Example: Rolling 48h Window
For a dynamic chart that automatically adapts to data availability:
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_ENTRY_ID
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
**Auto-Zoom Variant:**
For progressive zoom-in throughout the day:
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_ENTRY_ID
day: rolling_window_autozoom
level_type: rating_level
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
**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

View file

@ -0,0 +1,315 @@
# Chart Examples
This guide showcases the different chart configurations available through the `tibber_prices.get_apexcharts_yaml` action.
> **Quick Start:** Call the action with your desired parameters, copy the generated YAML, and paste it into your Lovelace dashboard!
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
:::info Finding your Entry ID (`entry_id`)
Every example below contains `entry_id: YOUR_ENTRY_ID`. This value identifies which Tibber home (integration instance) the action targets.
**In the Action UI (Developer Tools → Actions or the automation editor):** The `entry_id` field is a **dropdown** — just select your Tibber home and HA fills in the correct ID automatically.
**In YAML:** Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** (three-dot) menu, and choose **"Copy Config Entry ID"**. Paste the copied value in place of `YOUR_ENTRY_ID`.
:::
## Overview
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 |
**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
## All Chart Modes
### 1. Today's Prices (Static)
**When to use:** Simple daily price overview, no dynamic updates needed.
**Dependencies:** ApexCharts Card only
**Generate:**
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_ENTRY_ID
day: today
level_type: rating_level
highlight_best_price: true
```
**Screenshot:**
![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)
- ✅ Works with ApexCharts Card alone
**Note:** Tomorrow view (`day: tomorrow`) works identically to Today view, just showing tomorrow's data. All fixed day views (yesterday/today/tomorrow) use the same visualization approach.
---
### 2. Rolling 48h Window (Dynamic)
**When to use:** Always-current view that automatically switches between yesterday+today and today+tomorrow.
**Dependencies:** ApexCharts Card + Config Template Card
**Generate:**
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_ENTRY_ID
# Omit 'day' for rolling window
level_type: rating_level
highlight_best_price: true
```
**Screenshot:**
![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
- ✅ Updates automatically when tomorrow's data arrives
- ✅ 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
---
### 3. Rolling Window Auto-Zoom (Dynamic)
**When to use:** Real-time focus on remaining day - progressively zooms in as day advances.
**Dependencies:** ApexCharts Card + Config Template Card
**Generate:**
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_ENTRY_ID
day: rolling_window_autozoom
level_type: rating_level
highlight_best_price: true
```
**Screenshot:**
![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
- ✅ Perfect for real-time price monitoring
- ✅ 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)
- 18:00: Shows 16:00 → midnight (8h window)
- 23:45: Shows 21:45 → midnight (2.25h window)
This creates a "zooming in" effect that focuses on the most relevant remaining time.
---
## Comparison: Level Type Options
### Rating Level (3 series)
Based on **your personal price thresholds** (configured in Options Flow):
- **LOW** (Green): Below your "cheap" threshold
- **NORMAL** (Blue): Between thresholds
- **HIGH** (Red): Above your "expensive" threshold
**Best for:** Personal decision-making based on your budget
### Level (5 series)
Based on **absolute price ranges** (calculated from daily min/max):
- **VERY_CHEAP** (Dark Green): Bottom 20%
- **CHEAP** (Light Green): 20-40%
- **NORMAL** (Blue): 40-60%
- **EXPENSIVE** (Orange): 60-80%
- **VERY_EXPENSIVE** (Red): Top 20%
**Best for:** Objective price distribution visualization
---
## Dynamic Y-Axis Scaling
Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization:
**Without chart_metadata sensor (disabled):**
```
┌─────────────────────┐
│ │ ← Lots of empty space
│ ___ │
___/ \___
│_/ \_ │
├─────────────────────┤
0 100 ct
```
**With chart_metadata sensor (enabled):**
```
┌─────────────────────┐
│ ___ │ ← Y-axis fitted to data
___/ \___
│_/ \_ │
├─────────────────────┤
18 28 ct ← Optimal range
```
**Requirements:**
- ✅ The `sensor.<home_name>_chart_metadata` must be **enabled** (it's enabled by default!)
- ✅ That's it! The generated YAML automatically uses the sensor for dynamic scaling
**Important:** Do NOT disable the `chart_metadata` sensor if you want optimal Y-axis scaling in rolling window modes!
**Note:** Fixed day views (`today`, `tomorrow`) use ApexCharts' built-in auto-scaling and don't require the metadata sensor.
---
## Best Price Period Highlights
When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods:
**Example:**
```
Price
30│ ┌─────────┐ Normal prices
│ │ │
25│ ▓▓▓▓▓▓│ │ ← Best price period (shaded)
│ ▓▓▓▓▓▓│ │
20│─────▓▓▓▓▓▓│─────────│
│ ▓▓▓▓▓▓
└─────────────────────── Time
06:00 12:00 18:00
```
**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
- Can be disabled with `highlight_best_price: false`
---
## Prerequisites
### 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
```
### 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
```
**Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone!
---
## Tips & Tricks
### Customizing Colors
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
```
### Changing Chart Height
Add to the card configuration:
```yaml
type: custom:apexcharts-card
graph_span: 48h
header:
show: true
title: My Custom Title
apex_config:
chart:
height: 400 # Adjust height in pixels
```
### Combining with Other Cards
Wrap in a vertical stack for dashboard integration:
```yaml
type: vertical-stack
cards:
- type: entity
entity: sensor.<home_name>_current_electricity_price
- type: custom:apexcharts-card
# ... generated chart config
```
---
## Next Steps
- **[Actions Guide](actions.md)**: Complete documentation of `get_apexcharts_yaml` parameters
- **[Chart Metadata Sensor](sensors.md#chart-metadata)**: Learn about dynamic Y-axis scaling
- **[Period Calculation Guide](period-calculation.md)**: Configure best price period detection
---
## Screenshots
### Gallery
1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow)
![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)
3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect
![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).

View file

@ -0,0 +1,114 @@
# Core Concepts
Understanding the fundamental concepts behind the Tibber Prices integration.
## How Data Flows
```mermaid
flowchart LR
subgraph API["☁️ Tibber API"]
raw["Raw prices<br/>(quarter-hourly)"]
end
subgraph Integration["⚙️ Integration"]
direction TB
enrich["Enrichment<br/><small>24h averages, differences</small>"]
classify["Classification"]
enrich --> classify
end
subgraph Sensors["📊 Your Sensors"]
direction TB
prices["Price sensors<br/><small>current, min, max, avg</small>"]
ratings["Ratings & Levels<br/><small>LOW / NORMAL / HIGH</small>"]
periods["Periods<br/><small>best & peak windows</small>"]
trends["Trends & Volatility<br/><small>falling / stable / rising</small>"]
end
raw -->|every 15 min| enrich
classify --> prices
classify --> ratings
classify --> periods
classify --> trends
style API fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style Integration fill:#fff9e6,stroke:#ffb800,stroke-width:2px
style Sensors fill:#e6fff5,stroke:#00c853,stroke-width:2px
```
The integration fetches raw quarter-hourly prices from Tibber, enriches them with statistical context (averages, differences), and exposes the results as sensors you can use in automations and dashboards.
## Price Intervals
The integration works with **quarter-hourly intervals** (15 minutes):
- Each interval has a start time (e.g., 14:00, 14:15, 14:30, 14:45)
- Prices are fixed for the entire interval
- Synchronized with Tibber's smart meter readings
## Price Ratings
Prices are automatically classified into **rating levels**:
- **VERY_CHEAP** - Exceptionally low prices (great for energy-intensive tasks)
- **CHEAP** - Below average prices (good for flexible loads)
- **NORMAL** - Around average prices (regular consumption)
- **EXPENSIVE** - Above average prices (reduce consumption if possible)
- **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
## Price Periods
**Best Price Periods** and **Peak Price Periods** are automatically detected time windows:
- **Best Price Period** - Consecutive intervals with favorable prices (for scheduling energy-heavy tasks)
- **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)
See [Period Calculation](period-calculation.md) for detailed configuration.
## Statistical Analysis
The integration enriches every interval with context:
- **Trailing 24h Average** - Average price over the last 24 hours
- **Leading 24h Average** - Average price over the next 24 hours
- **Price Difference** - How much current price deviates from average (in %)
- **Volatility** - Price stability indicator (LOW, MEDIUM, HIGH)
This helps you understand if current prices are exceptional or typical.
## V-Shaped and U-Shaped Price Days
Some days show distinctive price curve shapes:
- **V-shaped**: Prices drop sharply, hit a brief minimum, then rise sharply again (common during short midday solar surplus)
- **U-shaped**: Prices drop to a low level and stay there for an extended period before rising (common during nighttime or extended low-demand periods)
**Why this matters:** On these days, the Best Price Period may be short (12 hours, covering only the absolute minimum), but prices can remain favorable for 46 hours. By combining [trend sensors](sensors.md#trend-sensors) with [price levels](sensors.md#core-price-sensors) in automations, you can ride the full cheap wave instead of only using the detected period.
See [Automation Examples → V-Shaped Days](automation-examples.md#understanding-v-shaped-price-days) for practical patterns.
## Multi-Home Support
You can add multiple Tibber homes to track prices for:
- Different locations
- Different electricity contracts
- Comparison between regions
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

View file

@ -0,0 +1,312 @@
# Configuration
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
## Initial Setup
After [installing](installation.md) the integration:
1. Go to **Settings → Devices & Services**
2. Click **+ Add Integration**
3. Search for **Tibber Price Information & Ratings**
4. **Enter your API token** from [developer.tibber.com](https://developer.tibber.com/settings/access-token)
5. **Select your Tibber home** from the dropdown (if you have multiple)
6. Click **Submit** — the integration starts fetching price data
The integration will immediately create sensors for your home. Data typically arrives within 12 minutes.
### Adding Additional Homes
If you have multiple Tibber homes (e.g., different locations):
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** → **Add another home**
3. Select the additional home from the dropdown
4. Each home gets its own set of sensors with unique entity IDs
## Options Flow (Configuration Wizard)
After initial setup, configure the integration through a multi-step wizard:
**Settings → Devices & Services → Tibber Prices → Configure**
```mermaid
flowchart LR
S1["① General"] --> S2["② Currency"]
S2 --> S3["③ Ratings"]
S3 --> S4["④ Levels"]
S4 --> S5["⑤ Volatility"]
S5 --> S6["⑥ Best Price"]
S6 --> S7["⑦ Peak Price"]
S7 --> S8["⑧ Trends"]
S8 --> S9["⑨ Chart"]
style S1 fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style S6 fill:#e6fff5,stroke:#00c853,stroke-width:2px
style S7 fill:#fff0f0,stroke:#ff5252,stroke-width:2px
```
All steps have sensible defaults — you can click through without changes and fine-tune later.
### Step 1: General Settings
- **Extended entity descriptions**: Show `description`, `long_description`, and `usage_tips` attributes on all sensors (useful for learning, can be disabled later to reduce attribute clutter)
- **Average sensor display**: Choose **Median** (typical price, spike-resistant) or **Mean** (mathematical average for cost calculations)
### Step 2: Currency Display
- **Base currency**: Shows prices as €/kWh, kr/kWh (e.g., 0.25 €/kWh)
- **Subunit**: Shows prices as ct/kWh, øre/kWh (e.g., 25.00 ct/kWh)
- Smart defaults: EUR → subunit (cents), NOK/SEK/DKK → base currency (kroner)
### Step 3: Price Rating Thresholds
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) |
### Step 4: Price Level Gap Tolerance
- **Gap tolerance** for Tibber's API-provided levels (VERY_CHEAP through VERY_EXPENSIVE)
- Smooths isolated level flickers: a single NORMAL surrounded by CHEAP → corrected to CHEAP
- Default: 1 interval tolerance
### Step 5: Price Volatility Thresholds
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 |
### 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
See [Period Calculation](period-calculation.md) for an in-depth explanation.
### Step 7: Peak Price Period
Mirrors Best Price configuration but for expensive windows. Detects periods to **avoid** consumption.
### Step 8: Price Trend Thresholds
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" |
Thresholds are [volatility-adaptive](sensors.md#trend-sensors): automatically widened on volatile days to prevent constant state changes.
### Step 9: Chart Data Export (Legacy)
Information page for the legacy chart data export sensor. For new setups, use the [get_chartdata action](actions.md) instead.
## Configuration Options
### Average Sensor Display Settings
**Location:** Settings → Devices & Services → Tibber Prices → Configure → Step 6
The integration allows you to choose how average price sensors display their values. This setting affects all average sensors (daily, 24h rolling, hourly smoothed, and future forecasts).
#### 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**
- Example: "What was my average cost per kWh today?"
#### Why This Matters
Consider a day with these hourly prices:
```
10, 12, 13, 15, 80 ct/kWh
```
- **Median = 13 ct/kWh** ← "Typical" price (middle value, ignores spike)
- **Mean = 26 ct/kWh** ← Average cost (spike pulls it up)
The median tells you the price was **typically** around 13 ct/kWh (4 out of 5 hours). The mean tells you if you consumed evenly, your **average cost** was 26 ct/kWh.
#### Automation-Friendly Design
**Both values are always available as attributes**, regardless of your display choice:
```yaml
# These attributes work regardless of display setting:
{{ state_attr('sensor.<home_name>_price_today', 'price_median') }}
{{ state_attr('sensor.<home_name>_price_today', 'price_mean') }}
```
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
#### Affected Sensors
This setting applies to:
- Daily average sensors (today, tomorrow)
- 24-hour rolling averages (trailing, leading)
- Hourly smoothed prices (current hour, next hour)
- Future forecast sensors (next 1h, 2h, 3h, ... 12h)
See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examples.
#### 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
- 📊 You track actual average costs over time
**Pro Tip:** Most users prefer **Median** for displays (more intuitive), but use `price_mean` attribute in cost calculation automations.
## Runtime Configuration Entities
The integration provides optional configuration entities that allow you to override period calculation settings at runtime through automations. These entities are **disabled by default** and can be enabled individually as needed.
### Available Configuration Entities
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 |
#### 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 |
### How Runtime Overrides Work
1. **Disabled (default):** The Options Flow setting is used
2. **Enabled:** The entity value overrides the Options Flow setting
3. **Value changes:** Trigger immediate period recalculation
4. **HA restart:** Entity values are restored automatically
### Viewing Entity Descriptions
Each configuration entity includes a detailed description attribute explaining what the setting does - the same information shown in the Options Flow.
**Note:** For **Number entities**, Home Assistant displays a history graph by default, which hides the attributes panel. To view the `description` attribute:
1. Go to **Developer Tools → States**
2. Search for the entity (e.g., `number.<home_name>_best_price_flexibility`)
3. Expand the attributes section to see the full description
**Switch entities** display their attributes normally in the entity details view.
### Example: Seasonal Automation
```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.<home_name>_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
If you frequently adjust these settings via automations or want to track configuration changes over time, the default behavior is fine.
However, if you prefer to **completely exclude** these entities from the recorder (no history graph, no database entries), add this to your `configuration.yaml`:
```yaml
recorder:
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
#### Price Sensor Statistics
The integration also minimizes long-term statistics growth for price sensors. Only 3 sensors write to the HA statistics database (which is never auto-purged):
- **Current Electricity Price** — Long-term price trend over weeks/months
- **Current Electricity Price (Energy Dashboard)** — Required for Energy Dashboard integration
- **Today's Average Price** — Seasonal price comparison
All other price sensors (forecasts, rolling averages, daily min/max, future averages) have long-term statistics disabled. Their **state history** (the step chart in the History panel) still works normally for ~10 days — only the smooth statistics line-chart on the entity detail page is absent for these sensors.
No configuration changes are needed — this optimization is built into the integration.

View file

@ -0,0 +1,188 @@
# Dashboard Examples
Beautiful dashboard layouts using Tibber Prices sensors.
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
## Basic Price Display Card
Simple card showing current price with dynamic color:
```yaml
type: entities
title: Current Electricity Price
entities:
- entity: sensor.<home_name>_current_electricity_price
name: Current Price
icon: mdi:flash
- entity: sensor.<home_name>_current_price_rating
name: Price Rating
- entity: sensor.<home_name>_next_electricity_price
name: Next Price
```
## Period Status Cards
Show when best/peak price periods are active:
```yaml
type: horizontal-stack
cards:
- type: entity
entity: binary_sensor.<home_name>_best_price_period
name: Best Price Active
icon: mdi:currency-eur-off
- type: entity
entity: binary_sensor.<home_name>_peak_price_period
name: Peak Price Active
icon: mdi:alert
```
## Custom Button Card Examples
### Price Level Card
```yaml
type: custom:button-card
entity: sensor.<home_name>_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)';
]]]
```
## Lovelace Layouts
### Compact Mobile View
Optimized for mobile devices:
```yaml
type: vertical-stack
cards:
- type: custom:mini-graph-card
entities:
- entity: sensor.<home_name>_current_electricity_price
name: Today's Prices
hours_to_show: 24
points_per_hour: 4
- type: glance
entities:
- entity: sensor.<home_name>_best_price_start
name: Best Period Starts
- entity: binary_sensor.<home_name>_best_price_period
name: Active Now
```
### Desktop Dashboard
Full-width layout for desktop:
```yaml
type: grid
columns: 3
square: false
cards:
- type: custom:apexcharts-card
# See chart-examples.md for ApexCharts config
- type: vertical-stack
cards:
- type: entities
title: Current Status
entities:
- sensor.<home_name>_current_electricity_price
- sensor.<home_name>_current_price_rating
- type: vertical-stack
cards:
- type: entities
title: Statistics
entities:
- sensor.<home_name>_price_today
- sensor.<home_name>_today_s_lowest_price
- sensor.<home_name>_today_s_highest_price
```
## Icon Color Integration
Using the `icon_color` attribute for dynamic colors:
```yaml
type: custom:mushroom-chips-card
chips:
- type: entity
entity: sensor.<home_name>_current_electricity_price
icon_color: "{{ state_attr('sensor.<home_name>_current_electricity_price', 'icon_color') }}"
- type: entity
entity: binary_sensor.<home_name>_best_price_period
icon_color: green
- type: entity
entity: binary_sensor.<home_name>_peak_price_period
icon_color: red
```
See [Icon Colors](icon-colors.md) for detailed color mapping.
## Picture Elements Dashboard
Advanced interactive dashboard:
```yaml
type: picture-elements
image: /local/electricity_dashboard_bg.png
elements:
- type: state-label
entity: sensor.<home_name>_current_electricity_price
style:
top: 20%
left: 50%
font-size: 32px
font-weight: bold
- type: state-badge
entity: binary_sensor.<home_name>_best_price_period
style:
top: 40%
left: 30%
# Add more elements...
```
## Auto-Entities Dynamic Lists
Automatically list all price sensors:
```yaml
type: custom:auto-entities
card:
type: entities
title: All Price Sensors
filter:
include:
- entity_id: "sensor.<home_name>_*_price"
exclude:
- state: unavailable
sort:
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

View file

@ -0,0 +1,180 @@
# Dynamic Icons
Many sensors in the Tibber Prices integration automatically change their icon based on their current state. This provides instant visual feedback about price levels, trends, and periods without needing to read the actual values.
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
## What are Dynamic Icons?
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)
The icons change automatically - no configuration needed!
## How to Check if a Sensor Has Dynamic Icons
To see which icon a sensor currently uses:
1. Go to **Developer Tools****States** in Home Assistant
2. Search for your sensor (e.g., `sensor.<home_name>_current_price_level`)
3. Look at the icon displayed in the entity row
4. Change conditions (wait for price changes) and check if the icon updates
**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`)
## Using Dynamic Icons in Your Dashboard
### Standard Entity Cards
Dynamic icons work automatically in standard Home Assistant cards:
```yaml
type: entities
entities:
- entity: sensor.<home_name>_current_price_level
- entity: sensor.<home_name>_current_price_rating
- entity: sensor.<home_name>_today_s_price_volatility
- entity: binary_sensor.<home_name>_best_price_period
```
The icons will update automatically as the sensor states change.
### Glance Card
```yaml
type: glance
entities:
- entity: sensor.<home_name>_current_price_level
name: Price Level
- entity: sensor.<home_name>_current_price_rating
name: Rating
- entity: binary_sensor.<home_name>_best_price_period
name: Best Price
```
### Custom Button Card
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price Level
show_state: true
# Icon updates automatically - no need to specify it!
```
### Mushroom Entity Card
```yaml
type: custom:mushroom-entity-card
entity: sensor.<home_name>_today_s_price_volatility
name: Price Volatility
# Icon changes automatically based on volatility level
```
## Overriding Dynamic Icons
If you want to use a fixed icon instead of the dynamic one:
### In Entity Cards
```yaml
type: entities
entities:
- entity: sensor.<home_name>_current_price_level
icon: mdi:lightning-bolt # Fixed icon, won't change
```
### In Custom Button Card
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_rating
name: Price Rating
icon: mdi:chart-line # Fixed icon overrides dynamic behavior
show_state: true
```
## Combining with Dynamic Colors
Dynamic icons work great together with dynamic colors! See the **[Dynamic Icon Colors Guide](icon-colors.md)** for examples.
**Example: Dynamic icon AND color**
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price
show_state: true
# Icon changes automatically (cheap/expensive cash icons)
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
```
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)
## Icon Behavior Details
### Binary Sensors
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
**Example:** `binary_sensor.<home_name>_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)
### 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
The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem.
## Troubleshooting
**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
**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`)
**Want different icons:**
- 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

View file

@ -0,0 +1,158 @@
# FAQ - Frequently Asked Questions
Common questions about the Tibber Prices integration.
## General Questions
### Why don't I see tomorrow's prices yet?
Tomorrow's prices are published by Tibber around **13:00 CET** (12:00 UTC in winter, 11:00 UTC in summer).
- **Before publication**: Sensors show `unavailable` or use today's data
- **After publication**: Integration automatically fetches new data within 15 minutes
- **No manual refresh needed** - polling happens automatically
### How often does the integration update data?
- **API Polling**: Every 15 minutes
- **Sensor Updates**: On quarter-hour boundaries (00, 15, 30, 45 minutes)
- **Cache**: Price data cached until midnight (reduces API load)
### Can I use multiple Tibber homes?
Yes! Use the **"Add another home"** option:
1. Settings → Devices & Services → Tibber Prices
2. Click "Configure" → "Add another home"
3. Select additional home from dropdown
4. Each home gets separate sensors with unique entity IDs
### Does this work without a Tibber subscription?
No, you need:
- Active Tibber electricity contract
- API token from [developer.tibber.com](https://developer.tibber.com/)
The integration is free, but requires Tibber as your electricity provider.
## Configuration Questions
### 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
### 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)
- **Relaxation**: Keep enabled (helps find periods on expensive days)
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%
4. Add more rating levels (include "NORMAL")
## Troubleshooting
### 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)
4. **Integration not loaded** → Restart Home Assistant
### 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
### Prices are in wrong currency or wrong units
**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)
- Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency
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`)
4. Tibber actually published data (check Tibber app)
## Automation Questions
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
### How do I run dishwasher during cheap period?
```yaml
automation:
- alias: "Dishwasher during Best Price"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_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.
### Can I avoid peak prices automatically?
Yes! Use Peak Price Period binary sensor:
```yaml
automation:
- alias: "Disable charging during peak prices"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_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)

View file

@ -0,0 +1,119 @@
---
comments: false
---
# Glossary
Quick reference for terms used throughout the documentation.
## A
**API Token**
: Your personal access key from Tibber. Get it at [developer.tibber.com](https://developer.tibber.com/settings/access-token).
**Attributes**
: Additional data attached to each sensor (timestamps, statistics, metadata). Access via `state_attr()` in templates.
## B
**Best Price Period**
: Automatically detected time window with favorable electricity prices. Ideal for scheduling dishwashers, heat pumps, EV charging.
**Binary Sensor**
: Sensor with ON/OFF state (e.g., "Best Price Period Active"). Used in automations as triggers.
## C
**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"**.
**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.
**Coordinator**
: Home Assistant component managing data fetching and updates. Polls Tibber API every 15 minutes.
## D
**Dynamic Icons**
: Icons that change based on sensor state (e.g., battery icons showing price level). See [Dynamic Icons](dynamic-icons.md).
## F
**Flex (Flexibility)**
: Configuration parameter controlling how strict period detection is. Higher flex = more periods found, but potentially at higher prices.
## I
**Interval**
: 15-minute time slot with fixed electricity price (00:00-00:15, 00:15-00:30, etc.).
## L
**Level**
: Price classification within a day (LOWEST, LOW, NORMAL, HIGH, HIGHEST). Based on daily min/max prices.
## M
**Min Distance**
: Threshold requiring periods to be at least X% below daily average. Prevents detecting "cheap" periods during expensive days.
## P
**Peak Price Period**
: Time window with highest electricity prices. Use to avoid heavy consumption.
**Price Info**
: Complete dataset with all intervals (yesterday, today, tomorrow) including enriched statistics.
## Q
**Quarter-Hourly**
: 15-minute precision (4 intervals per hour, 96 per day).
## R
**Rating**
: Statistical price classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE). Based on 24h averages and thresholds.
**Relaxation**
: Automatic loosening of period detection filters when target period count isn't met. Ensures you always get usable periods.
## S
**State**
: Current value of a sensor (e.g., price in ct/kWh, "ON"/"OFF" for binary sensors).
**State Class**
: Home Assistant classification for long-term statistics (MEASUREMENT, TOTAL, or none).
## T
**Trailing Average**
: Average price over the past 24 hours from current interval.
**Leading Average**
: Average price over the next 24 hours from current interval.
**Trend**
: Directional price movement indicator. Simple trends compare current price to future averages (1h12h). Current trend represents the ongoing price direction using a 3-hour outlook. Uses a 5-level scale: strongly_falling, falling, stable, rising, strongly_rising.
**Trend Hysteresis**
: Stability mechanism for trend change prediction. Requires 2 consecutive intervals confirming a different trend before reporting a change. Prevents false alarms from single-interval price spikes.
## V
**V-Shaped Day**
: Day with a V- or U-shaped price curve where prices drop to very cheap levels for an extended period. The Best Price Period covers only the absolute minimum, but favorable conditions may last much longer. See [V-Shaped Days](concepts.md#v-shaped-and-u-shaped-price-days).
**Volatility**
: Measure of price stability (LOW, MEDIUM, HIGH). High volatility = large price swings = good for timing optimization.
---
💡 **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

View file

@ -0,0 +1,449 @@
---
comments: false
---
# Dynamic Icon Colors
Many sensors in the Tibber Prices integration provide an `icon_color` attribute that allows you to dynamically color elements in your dashboard based on the sensor's state. This is particularly useful for visual dashboards where you want instant recognition of price levels or states.
**What makes icon_color special:** Instead of writing complex if/else logic to interpret the sensor state, you can simply use the `icon_color` value directly - it already contains the appropriate CSS color variable for the current state.
> **Related:** Many sensors also automatically change their **icon** based on state. See the **[Dynamic Icons Guide](dynamic-icons.md)** for details.
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
## What is icon_color?
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
### 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
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).
## Which Sensors Support icon_color?
Many sensors provide the `icon_color` attribute for dynamic styling. To see if a sensor has this attribute:
1. Go to **Developer Tools****States** in Home Assistant
2. Search for your sensor (e.g., `sensor.<home_name>_current_price_level`)
3. Look for `icon_color` in the attributes section
**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`)
The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray.
## When to Use icon_color vs. State Value
**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
**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
**Example of when NOT to use icon_color:**
```yaml
# ❌ DON'T: Converting icon_color requires if/else anyway
card:
- background: |
[[[
const color = entity.attributes.icon_color;
if (color === 'var(--success-color)') return 'rgba(76, 175, 80, 0.1)';
if (color === 'var(--error-color)') return 'rgba(244, 67, 54, 0.1)';
// ... more if statements
]]]
# ✅ DO: Interpret state directly if you need custom logic
card:
- background: |
[[[
const level = entity.state;
if (level === 'very_cheap' || level === 'cheap') return 'rgba(76, 175, 80, 0.1)';
if (level === 'very_expensive' || level === 'expensive') return 'rgba(244, 67, 54, 0.1)';
return 'transparent';
]]]
```
The advantage of `icon_color` is simplicity - if you need complex logic, you lose that advantage.
## How to Use icon_color in Your Dashboard
### Method 1: Custom Button Card (Recommended)
The [custom:button-card](https://github.com/custom-cards/button-card) from HACS supports dynamic icon colors.
**Example: Icon color only**
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price Level
show_state: true
icon: mdi:cash
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
```
**Example: Icon AND state value with same color**
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price Level
show_state: true
icon: mdi:cash
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
state:
- color: |
[[[
return entity.attributes.icon_color || 'var(--primary-text-color)';
]]]
- font-weight: bold
```
### Method 2: Entities Card with card_mod
Use Home Assistant's built-in entities card with card_mod for icon and state colors:
```yaml
type: entities
entities:
- entity: sensor.<home_name>_current_price_level
card_mod:
style:
hui-generic-entity-row:
$: |
state-badge {
color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }} !important;
}
.info {
color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }} !important;
}
```
### Method 3: Mushroom Cards
The [Mushroom cards](https://github.com/piitaya/lovelace-mushroom) support card_mod for icon and text colors:
**Icon color only:**
```yaml
type: custom:mushroom-entity-card
entity: binary_sensor.<home_name>_best_price_period
name: Best Price Period
icon: mdi:piggy-bank
card_mod:
style: |
ha-card {
--card-mod-icon-color: {{ state_attr('binary_sensor.<home_name>_best_price_period', 'icon_color') }};
}
```
**Icon and state value:**
```yaml
type: custom:mushroom-entity-card
entity: sensor.<home_name>_current_price_level
name: Price Level
card_mod:
style: |
ha-card {
--card-mod-icon-color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }};
--primary-text-color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }};
}
```
### Method 4: Glance Card with card_mod
Combine multiple sensors with dynamic colors:
```yaml
type: glance
entities:
- entity: sensor.<home_name>_current_price_level
- entity: sensor.<home_name>_today_s_price_volatility
- entity: binary_sensor.<home_name>_best_price_period
card_mod:
style: |
ha-card div.entity:nth-child(1) state-badge {
color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }} !important;
}
ha-card div.entity:nth-child(2) state-badge {
color: {{ state_attr('sensor.<home_name>_today_s_price_volatility', 'icon_color') }} !important;
}
ha-card div.entity:nth-child(3) state-badge {
color: {{ state_attr('binary_sensor.<home_name>_best_price_period', 'icon_color') }} !important;
}
```
## Complete Dashboard Example
Here's a complete example combining multiple sensors with dynamic colors:
```yaml
type: vertical-stack
cards:
# Current price status
- type: horizontal-stack
cards:
- type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Price Level
show_state: true
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
- type: custom:button-card
entity: sensor.<home_name>_current_price_rating
name: Price Rating
show_state: true
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
# Binary sensors for periods
- type: horizontal-stack
cards:
- type: custom:button-card
entity: binary_sensor.<home_name>_best_price_period
name: Best Price Period
show_state: true
icon: mdi:piggy-bank
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
- type: custom:button-card
entity: binary_sensor.<home_name>_peak_price_period
name: Peak Price Period
show_state: true
icon: mdi:alert-circle
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
# Volatility and trends
- type: horizontal-stack
cards:
- type: custom:button-card
entity: sensor.<home_name>_today_s_price_volatility
name: Volatility
show_state: true
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
- type: custom:button-card
entity: sensor.<home_name>_price_trend_3h
name: Next 3h Trend
show_state: true
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
```
## CSS Color Variables
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)
These automatically adapt to your theme's light/dark mode and custom color schemes.
### Using Custom Colors
If you want to override the theme colors with your own, you have two options:
#### Option 1: Use icon_color but Override in Your Theme
Define custom colors in your theme configuration (`themes.yaml`):
```yaml
my_custom_theme:
# Override standard variables
success-color: "#00C853" # Custom green
error-color: "#D32F2F" # Custom red
warning-color: "#F57C00" # Custom orange
info-color: "#0288D1" # Custom blue
```
The `icon_color` attribute will automatically use your custom theme colors.
#### Option 2: Interpret State Value Directly
Instead of using `icon_color`, read the sensor state and apply your own colors:
**Example: Custom colors for price level**
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price Level
show_state: true
icon: mdi:cash
styles:
icon:
- color: |
[[[
const level = entity.state;
if (level === 'very_cheap') return '#00E676'; // Bright green
if (level === 'cheap') return '#66BB6A'; // Light green
if (level === 'normal') return '#9E9E9E'; // Gray
if (level === 'expensive') return '#FF9800'; // Orange
if (level === 'very_expensive') return '#F44336'; // Red
return 'var(--state-icon-color)'; // Fallback
]]]
```
**Example: Custom colors for binary sensor**
```yaml
type: custom:button-card
entity: binary_sensor.<home_name>_best_price_period
name: Best Price Period
show_state: true
icon: mdi:piggy-bank
styles:
icon:
- color: |
[[[
// Use state directly, not icon_color
return entity.state === 'on' ? '#4CAF50' : '#9E9E9E';
]]]
card:
- background: |
[[[
return entity.state === 'on' ? 'rgba(76, 175, 80, 0.1)' : 'transparent';
]]]
```
**Example: Custom colors for volatility**
```yaml
type: custom:button-card
entity: sensor.<home_name>_today_s_price_volatility
name: Volatility Today
show_state: true
styles:
icon:
- color: |
[[[
const volatility = entity.state;
if (volatility === 'low') return '#4CAF50'; // Green
if (volatility === 'moderate') return '#2196F3'; // Blue
if (volatility === 'high') return '#FF9800'; // Orange
if (volatility === 'very_high') return '#F44336'; // Red
return 'var(--state-icon-color)';
]]]
```
**Example: Custom colors for price rating**
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_rating
name: Price Rating
show_state: true
styles:
icon:
- color: |
[[[
const rating = entity.state;
if (rating === 'low') return '#00C853'; // Dark green
if (rating === 'normal') return '#78909C'; // Blue-gray
if (rating === 'high') return '#D32F2F'; // Dark red
return 'var(--state-icon-color)';
]]]
```
### Which Approach Should You Use?
| Use Case | Recommended Approach |
| ------------------------------------- | ---------------------------------- |
| Want theme-consistent colors | ✅ Use `icon_color` directly |
| Want light/dark mode support | ✅ Use `icon_color` directly |
| Want custom theme colors | ✅ Override CSS variables in theme |
| Want specific hardcoded colors | ⚠️ Interpret state value directly |
| Multiple themes with different colors | ✅ Use `icon_color` directly |
**Recommendation:** Use `icon_color` whenever possible for better theme integration. Only interpret the state directly if you need very specific color values that shouldn't change with themes.
## Troubleshooting
**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
**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
**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`
## 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

View file

@ -0,0 +1,69 @@
# Installation
## HACS Installation (Recommended)
[HACS](https://hacs.xyz/) (Home Assistant Community Store) is the easiest way to install and keep the integration up to date.
### Prerequisites
- Home Assistant 2025.10.0 or newer
- [HACS](https://hacs.xyz/docs/use/) installed and configured
- A [Tibber API token](https://developer.tibber.com/settings/access-token)
### Steps
1. Open HACS in your Home Assistant sidebar
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**
5. Click **Add**
6. Find **Tibber Price Information & Ratings** in the integration list
7. Click **Download**
8. **Restart Home Assistant**
9. Continue with [Configuration](configuration.md)
### Updating
HACS will show a notification when updates are available:
1. Open HACS → **Integrations**
2. Find **Tibber Price Information & Ratings**
3. Click **Update**
4. **Restart Home Assistant**
## Manual Installation
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/
└── ...
```
4. **Restart Home Assistant**
5. Continue with [Configuration](configuration.md)
## After Installation
Once installed and restarted, add the integration:
1. Go to **Settings → Devices & Services**
2. Click **+ Add Integration**
3. Search for **Tibber Price Information & Ratings**
4. Enter your [Tibber API token](https://developer.tibber.com/settings/access-token)
5. Select your Tibber home
6. The integration will start fetching price data
See the [Configuration Guide](configuration.md) for detailed setup options.

View file

@ -0,0 +1,59 @@
---
comments: false
---
# User Documentation
Welcome to the **Tibber Prices custom integration for Home Assistant**! This community-developed integration enhances your Home Assistant installation with detailed electricity price data from Tibber, featuring quarter-hourly precision, statistical analysis, and intelligent ratings.
:::info Not affiliated with Tibber
This is an independent, community-maintained custom integration. It is **not** an official Tibber product and is **not** affiliated with or endorsed by Tibber AS.
:::
## 📚 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
## 🚀 Quick Start
1. **Install via HACS** (add as custom repository)
2. **Add Integration** in Home Assistant → Settings → Devices & Services
3. **Enter Tibber API Token** (get yours at [developer.tibber.com](https://developer.tibber.com/))
4. **Configure Price Thresholds** (optional, defaults work for most users)
5. **Start Using Sensors** in automations, dashboards, and scripts!
## ✨ 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
## 🔗 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/)
## 🤝 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
---
**Note:** These guides are for end users. If you want to contribute to development, see the [Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/).

View file

@ -0,0 +1,830 @@
# Period Calculation
Learn how Best Price and Peak Price periods work, and how to configure them for your needs.
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
## 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
### What Are Price Periods?
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
### Default Behavior
Out of the box, the integration:
1. **Best Price**: Finds cheapest 1-hour+ windows that are at least 5% below the daily average
2. **Peak Price**: Finds most expensive 30-minute+ windows that are at least 5% above the daily average
3. **Relaxation**: Automatically loosens filters if not enough periods are found
**Most users don't need to change anything!** The defaults work well for typical use cases.
<details>
<summary> Why do Best Price and Peak Price have different defaults?</summary>
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.
</details>
### Example Timeline
```
00:00 ████████████████ Best Price Period (cheap prices)
04:00 ░░░░░░░░░░░░░░░░ Normal
08:00 ████████████████ Peak Price Period (expensive prices)
12:00 ░░░░░░░░░░░░░░░░ Normal
16:00 ████████████████ Peak Price Period (expensive prices)
20:00 ████████████████ Best Price Period (cheap prices)
```
---
## How It Works
### The Basic Idea
Each day, the integration analyzes all 96 quarter-hourly price intervals and identifies **continuous time ranges** that meet specific criteria.
```mermaid
flowchart TD
A["96 intervals per day"] --> B{"① Flexibility<br/><small>Close to MIN/MAX?</small>"}
B -->|Yes| C{"② Distance<br/><small>Meaningfully different<br/>from average?</small>"}
B -->|No| X1["❌ excluded"]
C -->|Yes| D{"③ Duration<br/><small>≥ 60 min?</small>"}
C -->|No| X2["❌ excluded"]
D -->|Yes| E{"④ Level filter<br/><small>(optional)</small>"}
D -->|No| X3["❌ too short"]
E -->|Pass| F["⑤ Spike smoothing"]
E -->|Fail| X4["❌ filtered"]
F --> G["✅ Period found"]
style A fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style G fill:#e6fff5,stroke:#00c853,stroke-width:2px
style X1 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
style X2 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
style X3 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
style X4 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
```
Think of it like this:
1. **Find potential windows** - Times close to the daily MIN (Best Price) or MAX (Peak Price)
2. **Filter by quality** - Ensure they're meaningfully different from average
3. **Check duration** - Must be long enough to be useful
4. **Apply preferences** - Optional: only show stable prices, avoid mediocre times
### Step-by-Step Process
#### 1. Define the Search Range (Flexibility)
**Best Price:** How much MORE than the daily minimum can a price be?
```
Daily MIN: 20 ct/kWh
Flexibility: 15% (default)
→ Search for times ≤ 23 ct/kWh (20 + 15%)
```
**Peak Price:** How much LESS than the daily maximum can a price be?
```
Daily MAX: 40 ct/kWh
Flexibility: -15% (default)
→ Search for times ≥ 34 ct/kWh (40 - 15%)
```
**Why flexibility?** Prices rarely stay at exactly MIN/MAX. Flexibility lets you capture realistic time windows.
#### 2. Ensure Quality (Distance from Average)
Periods must be meaningfully different from the daily average:
```
Daily AVG: 30 ct/kWh
Minimum distance: 5% (default)
Best Price: Must be ≤ 28.5 ct/kWh (30 - 5%)
Peak Price: Must be ≥ 31.5 ct/kWh (30 + 5%)
```
**Why?** This prevents marking mediocre times as "best" just because they're slightly below average.
#### 3. Check Duration
Periods must be long enough to be practical:
```
Default: 60 minutes minimum
45-minute period → Discarded
90-minute period → Kept ✓
```
#### 4. Apply Optional Filters
You can optionally require:
- **Absolute quality** (level filter) - "Only show if prices are CHEAP/EXPENSIVE (not just below/above average)"
#### 5. Automatic Price Spike Smoothing
Isolated price spikes are automatically detected and smoothed to prevent unnecessary period fragmentation:
```
Original prices: 18, 19, 35, 20, 19 ct ← 35 ct is an isolated outlier
Smoothed: 18, 19, 19, 20, 19 ct ← Spike replaced with trend prediction
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
### Visual Example
**Timeline for a typical day:**
```
Hour: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Price: 18 19 20 28 29 30 35 34 33 32 30 28 25 24 26 28 30 32 31 22 21 20 19 18
Daily MIN: 18 ct | Daily MAX: 35 ct | Daily AVG: 26 ct
Best Price (15% flex = ≤20.7 ct):
████████ ████████████████
00:00-03:00 (3h) 19:00-24:00 (5h)
Peak Price (-15% flex = ≥29.75 ct):
████████████████████████
06:00-11:00 (5h)
```
---
## Configuration Guide
### Basic Settings
#### Flexibility
**What:** How far from MIN/MAX to search for periods
**Default:** 15% (Best Price), -15% (Peak Price)
**Range:** 0-100%
```yaml
best_price_flex: 15 # Can be up to 15% more expensive than daily MIN
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
**💡 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.
#### Minimum Period Length
**What:** How long a period must be to show it
**Default:** 60 minutes (Best Price), 30 minutes (Peak Price)
**Range:** 15-240 minutes
```yaml
best_price_min_period_length: 60
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)
#### Distance from Average
**What:** How much better than average a period must be
**Default:** 5%
**Range:** 0-20%
```yaml
best_price_min_distance_from_avg: 5
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
** 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.
### Optional Filters
#### Level Filter (Absolute Quality)
**What:** Only show periods with CHEAP/EXPENSIVE intervals (not just below/above average)
**Default:** `any` (disabled)
**Options:** `any` | `cheap` | `very_cheap` (Best Price) | `expensive` | `very_expensive` (Peak Price)
```yaml
best_price_max_level: any # Show any period below average
best_price_max_level: cheap # Only show if at least one interval is CHEAP
```
**Use case:** "Only notify me when prices are objectively cheap/expensive"
** Volatility Thresholds:** The level filter also supports volatility-based levels (`volatility_low`, `volatility_medium`, `volatility_high`). These use **fixed internal thresholds** (LOW < 10%, MEDIUM < 20%, HIGH 20%) that are separate from the sensor volatility thresholds you configure in the UI. This separation ensures that changing sensor display preferences doesn't affect period calculation behavior.
#### Gap Tolerance (for Level Filter)
**What:** Allow some "mediocre" intervals within an otherwise good period
**Default:** 0 (strict)
**Range:** 0-10
```yaml
best_price_max_level: cheap
best_price_max_level_gap_count: 2 # Allow up to 2 NORMAL intervals per period
```
**Use case:** "Don't split periods just because one interval isn't perfectly CHEAP"
### Tweaking Strategy: What to Adjust First?
When you're not happy with the default behavior, adjust settings in this order:
#### 1. **Start with Relaxation (Easiest)**
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!
```
**Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning.
#### 2. **Adjust Period Length (Simple)**
If periods are too short/long for your use case:
```yaml
best_price_min_period_length: 90 # Increase from 60 for longer periods
# OR
best_price_min_period_length: 45 # Decrease from 60 for shorter periods
```
**Safe to change:** This only affects duration, not price selection logic.
#### 3. **Fine-tune Flexibility (Moderate)**
If you consistently want more/fewer periods:
```yaml
best_price_flex: 20 # Increase from 15% for more periods
# OR
best_price_flex: 10 # Decrease from 15% for stricter selection
```
**⚠️ Watch out:** Values >25% may conflict with distance filter. Use relaxation instead.
#### 4. **Adjust Distance from Average (Advanced)**
Only if periods seem "mediocre" (not really cheap/expensive):
```yaml
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.
#### 5. **Enable Level Filter (Expert)**
Only if you want absolute quality requirements:
```yaml
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!**
### Common Mistakes to Avoid
**Don't increase flexibility to >30% manually** → Use relaxation instead
**Don't combine high distance (>10%) with strict level filter** → Too restrictive
**Don't disable relaxation with strict filters** → You'll get zero periods on some days
**Don't change all settings at once** → Adjust one at a time and observe results
**Do use defaults + relaxation** → Works for 90% of cases
**Do adjust one setting at a time** → Easier to understand impact
**Do check sensor attributes** → Shows why periods were/weren't found
---
## Understanding Relaxation
### What Is Relaxation?
Sometimes, strict filters find too few periods (or none). **Relaxation automatically loosens filters** until a minimum number of periods is found.
### How to Enable
```yaml
enable_min_periods_best: true
min_periods_best: 2 # Try to find at least 2 periods per day
relaxation_attempts_best: 11 # Flex levels to test (default: 11 steps = 22 filter combinations)
```
** Good news:** Relaxation is **enabled by default** with sensible settings. Most users don't need to change anything here!
Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods. Both sliders accept 1-12 attempts, and the default of 11 flex levels translates to 22 filter-combination tries (11 flex levels × 2 filter combos) for each of Best and Peak calculations. Lower it for quick feedback, or raise it when either sensor struggles to hit the minimum-period target on volatile days.
### 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 ✓
**Each day gets exactly the flexibility it needs!**
### How It Works (Adaptive Matrix)
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)
#### Phase Matrix
For each day, the system tries:
```mermaid
flowchart TD
Start["Start: base flex<br/><small>(e.g. 15%)</small>"] --> A1
subgraph Attempt1["Attempt 1 — flex 15%"]
A1["Your filters"] -->|not enough| A2["Level = any"]
end
A2 -->|not enough| B1
subgraph Attempt2["Attempt 2 — flex 18%"]
B1["Your filters"] -->|not enough| B2["Level = any"]
end
B2 -->|not enough| C1
subgraph Attempt3["Attempt 3 — flex 21%"]
C1["Your filters"] --> C2["Level = any"]
end
C1 -->|"✅ enough"| Done
A1 -->|"✅ enough"| Done
A2 -->|"✅ enough"| Done
B1 -->|"✅ enough"| Done
B2 -->|"✅ enough"| Done
C2 -->|"✅ / not enough → next …"| Done
Done["✅ Done<br/><small>stops at first success</small>"]
style Start fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style Done fill:#e6fff5,stroke:#00c853,stroke-width:2px
style Attempt1 fill:#f0f9ff,stroke:#00b9e7
style Attempt2 fill:#fff9e6,stroke:#ffb800
style Attempt3 fill:#fff0f0,stroke:#ff8a80
```
Each attempt adds +3% flexibility and tries two filter combinations. The system **stops as soon as enough periods are found** — it doesn't keep trying the full matrix.
### 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)
#### Per-Day Independence
**Critical:** Each day relaxes **independently**:
```
Day 1: Finds 2 periods with flex 15% (original) → No relaxation needed
Day 2: Needs flex 21% + level=any → Uses relaxed settings
Day 3: Finds 2 periods with flex 15% (original) → No relaxation needed
```
**Why?** Price patterns vary daily. Some days have clear cheap/expensive windows (strict filters work), others don't (relaxation needed).
---
## Common Scenarios
### Scenario 1: Simple Best Price (Default)
**Goal:** Find the cheapest time each day to run dishwasher
**Configuration:**
```yaml
# Use defaults - no configuration needed!
best_price_flex: 15 # (default)
best_price_min_period_length: 60 # (default)
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
**Automation example:**
```yaml
automation:
- trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
action:
- service: switch.turn_on
target:
entity_id: switch.dishwasher
```
---
## Troubleshooting
### Fewer Periods Than Configured
**Symptom:** You configured `min_periods_best: 2` but the sensor shows fewer periods on some days, and the attributes contain `flat_days_detected: 1` or `relaxation_incomplete: true`.
**If `flat_days_detected` is present:**
This is **expected behavior** on days with very uniform electricity prices. When prices vary by less than ~10% across the day (e.g. on sunny spring days with high solar generation), there is no meaningful second "cheap window" all hours are equally cheap. The integration automatically reduces the target to 1 period for that day.
```yaml
min_periods_configured: 2
periods_found_total: 1
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.
**If `relaxation_incomplete` is present (without `flat_days_detected`):**
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
```
2. **Reduce minimum period count**
```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
---
### No Periods Found
**Symptom:** `binary_sensor.<home_name>_best_price_period` never turns "on"
**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
```
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
3. **Try increasing flexibility slightly**
```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
```
### Periods Split Into Small Pieces
**Symptom:** Many short periods instead of one long period
**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
```
2. **Slightly increase flexibility**
```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
### Understanding Sensor Attributes
**Key attributes to check:**
```yaml
# Entity: binary_sensor.<home_name>_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
# 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
# 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
# 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
```
#### What the diagnostic attributes mean
**`min_periods_configured` / `periods_found_total`**
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 ✅
```
```yaml
min_periods_configured: 2
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.
**`flat_days_detected`**
This is the most important diagnostic for days with very uniform prices (e.g. sunny spring/summer days with high solar generation):
```yaml
min_periods_configured: 2
periods_found_total: 1
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.
**This is expected and correct behavior**, not a problem. It prevents the sensor from generating artificial periods that don't represent genuinely cheaper windows.
**`relaxation_incomplete`**
This flag appears when even after all relaxation attempts, at least one day could not reach the configured minimum number of periods:
```yaml
min_periods_configured: 2
periods_found_total: 1
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
### Midnight Price Classification Changes
**Symptom:** A Best Price period at 23:45 suddenly changes to Peak Price at 00:00 (or vice versa), even though the absolute price barely changed.
**Why This Happens:**
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
4. **Relative Classification:** Periods are classified based on their **position within the day's price range**, not absolute prices
**Example:**
```yaml
# Day 1 (low volatility, narrow range)
Price range: 18-22 ct/kWh (4 ct span)
Daily average: 20 ct/kWh
23:45: 18.5 ct/kWh → 7.5% below average → BEST PRICE ✅
# Day 2 (low volatility, narrow range)
Price range: 17-21 ct/kWh (4 ct span)
Daily average: 19 ct/kWh
00:00: 18.6 ct/kWh → 2.1% below average → PEAK PRICE ❌
# Observation: Absolute price barely changed (18.5 → 18.6 ct)
# But relative position changed dramatically:
# - Day 1: Near the bottom of the range
# - Day 2: Near the middle/top of the range
```
**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
**How to Detect:**
Check the volatility sensors to understand if a period flip is meaningful:
```yaml
# Check daily volatility (available in integration)
sensor.<home_name>_today_s_price_volatility: 8.2% # Low volatility
sensor.<home_name>_tomorrow_s_price_volatility: 7.9% # Also low
# Low volatility (< 15%) means:
# - Small absolute price differences between periods
# - Classification changes may not be economically significant
# - Consider ignoring period classification on such days
```
**Handling in Automations:**
You can make your automations volatility-aware:
```yaml
# Option 1: Only act on high-volatility days
automation:
- alias: "Dishwasher - Best Price (High Volatility Only)"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
- condition: numeric_state
entity_id: sensor.<home_name>_today_s_price_volatility
above: 15 # Only act if volatility > 15%
action:
- service: switch.turn_on
entity_id: switch.dishwasher
# Option 2: Check absolute price, not just classification
automation:
- alias: "Heat Water - Cheap Enough"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
- condition: numeric_state
entity_id: sensor.<home_name>_current_electricity_price
below: 20 # Absolute threshold: < 20 ct/kWh
action:
- service: switch.turn_on
entity_id: switch.water_heater
# Option 3: Use per-period day volatility (available on period sensors)
automation:
- alias: "EV Charging - Volatility-Aware"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
# Check if the period's day has meaningful volatility
- condition: template
value_template: >
{{ state_attr('binary_sensor.<home_name>_best_price_period', 'day_volatility_%') | float(0) > 15 }}
action:
- service: switch.turn_on
entity_id: switch.ev_charger
```
**Available Per-Period Attributes:**
Each period sensor exposes day volatility and price statistics:
```yaml
binary_sensor.<home_name>_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
```
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
---
## Advanced Topics
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
### Quick Reference
**Configuration Parameters:**
| Parameter | Default | Range | Purpose |
| ---------------------------------- | ------- | ---------------- | ------------------------------ |
| `best_price_flex` | 15% | 0-100% | Search range from daily MIN |
| `best_price_min_period_length` | 60 min | 15-240 | Minimum duration |
| `best_price_min_distance_from_avg` | 5% | 0-20% | Quality threshold |
| `best_price_max_level` | any | any/cheap/vcheap | Absolute quality |
| `best_price_max_level_gap_count` | 0 | 0-10 | Gap tolerance |
| `enable_min_periods_best` | true | true/false | Enable relaxation |
| `min_periods_best` | 2 | 1-10 | Target periods per day |
| `relaxation_attempts_best` | 11 | 1-12 | Flex levels (attempts) per day |
**Peak Price:** Same parameters with `peak_price_*` prefix (defaults: flex=-15%, same otherwise)
### Price Levels Reference
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
---
**Last updated:** November 20, 2025
**Integration version:** 2.0+

View file

@ -0,0 +1,873 @@
---
comments: false
---
# Sensors
> **Tip:** Many sensors have dynamic icons and colors! See the **[Dynamic Icons Guide](dynamic-icons.md)** and **[Dynamic Icon Colors Guide](icon-colors.md)** to enhance your dashboards.
> **Entity ID tip:** `<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Example suffixes below use the English display names (en.json) as a baseline. You can find the real ID in **Settings → Devices & Services → Entities** (or **Developer Tools → States**).
## Binary Sensors
### Best Price Period & Peak Price Period
These binary sensors indicate when you're in a detected best or peak price period. See the **[Period Calculation Guide](period-calculation.md)** for a detailed explanation of how these periods are calculated and configured.
**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
Both sensors include rich attributes with period details, intervals, relaxation status, and more.
## Core Price Sensors
### Average Price Sensors
The integration provides several sensors that calculate average electricity prices over different time windows. These sensors show a **typical** price value that represents the overall price level, helping you make informed decisions about when to use electricity.
#### 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 |
#### Configurable Display: Median vs Mean
All average sensors support **two different calculation methods** for the state value:
- **Median** (default): The "middle value" when all prices are sorted. Resistant to extreme price spikes, shows the **typical** price level you experienced.
- **Arithmetic Mean**: The mathematical average including all prices. Better for **cost calculations** but affected by extreme spikes.
**Why two values matter:**
```yaml
# Example price data for one day:
# Prices: 10, 12, 13, 15, 80 ct/kWh (one extreme spike)
#
# Median = 13 ct/kWh ← "Typical" price level (middle value)
# Mean = 26 ct/kWh ← Mathematical average (affected by spike)
```
The median shows you what price level was **typical** during that period, while the mean shows the actual **average cost** if you consumed evenly throughout the period.
#### Configuring the Display
You can choose which value is displayed in the sensor state:
1. Go to **Settings → Devices & Services → Tibber Prices**
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
**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.
#### Using Both Values in Automations
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.<home_name>_price_today', 'price_median') %}
{% set mean = state_attr('sensor.<home_name>_price_today', 'price_mean') %}
{% set current = states('sensor.<home_name>_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 %}
```
#### Practical Examples
**Example 1: Smart dishwasher control**
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.<home_name>_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.<home_name>_current_electricity_price') | float %}
{% set median = state_attr('sensor.<home_name>_price_today', 'price_median') | float %}
{{ current < (median * 0.8) }}
action:
- service: switch.turn_on
entity_id: switch.dishwasher
```
**Example 2: Cost-aware heating control**
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.<home_name>_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**
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.<home_name>_current_electricity_price') | float %}
{% set trailing_avg = state_attr('sensor.<home_name>_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 |
**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.
The calculation is based on the **Coefficient of Variation (CV)**, a standardized statistical measure defined as:
`CV = (Standard Deviation / aAithmetic Mean) * 100%`
This results in a percentage that shows how much prices deviate from the average. A low CV means stable prices, while a high CV indicates significant price swings and thus, a high potential for saving money by shifting consumption.
The sensor's state can be `low`, `moderate`, `high`, or `very_high`, based on configurable thresholds.
### 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 |
### 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%
### Key Attributes
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` |
### Usage in Automations & Best Practices
You can use the volatility sensor to decide if a price-based optimization is worth it. For example, if your solar battery has conversion losses, you might only want to charge and discharge it on days with high volatility.
**Best Practice: Use the `price_volatility` Attribute**
For automations, it is strongly recommended to use the `price_volatility` attribute instead of the sensor's main state.
- **Why?** The main `state` of the sensor is translated into your Home Assistant language (e.g., "Hoch" in German). If you change your system language, automations based on this state will break. The `price_volatility` attribute is **always in lowercase English** (`"low"`, `"moderate"`, `"high"`, `"very_high"`) and therefore provides a stable, language-independent value.
**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.<home_name>_today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }}
action:
- service: input_boolean.turn_on
entity_id: input_boolean.battery_optimization_enabled
```
---
**Avoid Hard-Coding Numeric Thresholds**
You might be tempted to use the numeric `price_coefficient_variation_%` attribute directly in your automations. This is not recommended.
- **Why?** The integration provides central configuration options for the volatility thresholds. By using the classified `price_volatility` attribute, your automations automatically adapt if you decide to change what you consider "high" volatility (e.g., changing the threshold from 30% to 35%). Hard-coding values means you would have to find and update them in every single automation.
**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.<home_name>_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.
## Rating Sensors
Rating sensors classify prices relative to the **trailing 24-hour average**, answering: "Is the current price cheap, normal, or expensive compared to recent history?"
### How Ratings Work
The integration calculates a **percentage difference** between the current price and the trailing 24-hour average:
```
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 |
**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.
```mermaid
stateDiagram-v2
direction LR
LOW: 🟢 LOW<br/><small>price ≤ 10%</small>
NORMAL: 🟡 NORMAL<br/><small>10% … +10%</small>
HIGH: 🔴 HIGH<br/><small>price ≥ +10%</small>
LOW --> NORMAL: crosses 8%<br/><small>(hysteresis)</small>
NORMAL --> LOW: drops below 10%
NORMAL --> HIGH: rises above +10%
HIGH --> NORMAL: crosses +8%<br/><small>(hysteresis)</small>
```
> **The 2% gap** between entering (10%) and leaving (8%) a state prevents the sensor from flickering back and forth when prices hover near a threshold.
### 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 |
| **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 |
### 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) |
**Which to use?**
- **Automations**: Use **ratings** (3 simple states, configurable thresholds, hysteresis)
- **Dashboards**: Use **levels** (5 color-coded states, more visual granularity)
- **Advanced automations**: Combine both (e.g., "LOW rating AND VERY_CHEAP level")
### 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` |
### Usage in Automations
**Best Practice:** Always use the `rating_level` attribute (lowercase English) instead of the sensor state (which is translated to your HA language):
```yaml
# ✅ Correct — language-independent
condition:
- condition: template
value_template: >
{{ state_attr('sensor.<home_name>_current_price_rating', 'rating_level') == 'low' }}
# ❌ Avoid — breaks when HA language changes
condition:
- condition: state
entity_id: sensor.<home_name>_current_price_rating
state: "Low" # "Niedrig" in German, "Lav" in Norwegian...
```
### Configuration
Rating thresholds can be adjusted in the options flow:
1. Go to **Settings → Devices & Services → Tibber Prices → Configure**
2. Navigate to **Price Rating Thresholds**
3. Adjust LOW/HIGH thresholds, hysteresis, and gap tolerance
See [Configuration](configuration.md#step-3-price-rating-thresholds) for details.
## Level Sensors
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 |
### Available Level Sensors
| 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) |
**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).
## Min/Max Sensors
These sensors show the lowest and highest prices for calendar days and rolling windows:
### Daily Min/Max
| Sensor | Description |
|--------|-------------|
| **Today's Lowest Price** | Minimum price today (00:0023:59) |
| **Today's Highest Price** | Maximum price today (00:0023: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 |
| **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 |
### 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 |
## Timing Sensors
Timing sensors provide **real-time information about Best Price and Peak Price periods**: when they start, end, how long they last, and your progress through them.
```mermaid
stateDiagram-v2
direction LR
IDLE: ⏸️ IDLE<br/><small>No active period</small>
ACTIVE: ▶️ ACTIVE<br/><small>In period</small>
GRACE: ⏳ GRACE<br/><small>60s buffer</small>
IDLE --> ACTIVE: period starts
ACTIVE --> GRACE: period ends
GRACE --> IDLE: 60s elapsed
GRACE --> ACTIVE: new period starts<br/><small>(within grace)</small>
```
**IDLE** = waiting for next period (shows countdown via `next_in_minutes`). **ACTIVE** = inside a period (shows `progress` 0100% and `remaining_minutes`). **GRACE** = short buffer after a period ends, allowing back-to-back periods to merge seamlessly.
### Available Timing Sensors
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** | 0100% 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
**Show countdown to next cheap window:**
```yaml
type: custom:mushroom-entity-card
entity: sensor.<home_name>_best_price_next_in_minutes
name: Next Cheap Window
icon: mdi:clock-fast
```
**Display period progress bar:**
```yaml
type: custom:bar-card
entity: sensor.<home_name>_best_price_progress
name: Best Price Progress
min: 0
max: 100
severity:
- from: 0
to: 50
color: green
- from: 50
to: 80
color: orange
- from: 80
to: 100
color: red
```
**Automation: notify when period is almost over:**
```yaml
automation:
- alias: "Warn: Best Price Ending Soon"
trigger:
- platform: numeric_state
entity_id: sensor.<home_name>_best_price_remaining_minutes
below: 15
condition:
- condition: numeric_state
entity_id: sensor.<home_name>_best_price_remaining_minutes
above: 0
action:
- service: notify.mobile_app
data:
title: "Best Price Ending Soon"
message: "Only {{ states('sensor.<home_name>_best_price_remaining_minutes') }} minutes left!"
```
## Trend Sensors
Trend sensors help you understand **where prices are heading**. They answer the question: "Should I use electricity now, or wait?"
The integration provides two families of trend sensors for different use cases:
### Simple Trend Sensors (1h12h)
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 |
| **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:
```mermaid
stateDiagram-v2
direction LR
SF: ⬇️⬇️ strongly_falling<br/><small>2 · future ≤ 9%</small>
F: ⬇️ falling<br/><small>1 · future ≤ 3%</small>
S: ➡️ stable<br/><small>0 · within ±3%</small>
R: ⬆️ rising<br/><small>+1 · future ≥ +3%</small>
SR: ⬆️⬆️ strongly_rising<br/><small>+2 · future ≥ +9%</small>
SF --> F: price recovers
F --> S: approaches average
S --> R: future rises
R --> SR: accelerates
SR --> R: slows down
R --> S: stabilizes
S --> F: future drops
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 |
**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` |
**Tip:** The `trend_value` attribute (`-2` to `+2`) is ideal for automations — use numeric comparisons instead of matching translated state strings.
### Current Price Trend
**Entity ID:** `sensor.<home_name>_current_price_trend`
This sensor shows the **currently active trend direction** based on a 3-hour future outlook with volatility-adaptive thresholds.
Unlike the simple trend sensors that always compare current price vs future average, the current price trend represents the **ongoing trend** — it remains stable between updates and only changes when the underlying price direction actually shifts.
**States:** Same 5-level scale as simple trends.
**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` |
### Next Price Trend Change
**Entity ID:** `sensor.<home_name>_next_price_trend_change`
This sensor predicts **when the current trend will change** by scanning future intervals. It requires 3 consecutive intervals (configurable: 26) confirming the new trend before reporting a change (hysteresis), which prevents false alarms from short-lived price spikes.
**Important:** Only **direction changes** count as trend changes. The five states are grouped into three directions:
| Direction | States |
|-----------|--------|
| **falling** | `strongly_falling`, `falling` |
| **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`.
**State:** Timestamp of the next trend change (or unavailable if no change predicted).
**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` |
### How to Use Trend Sensors for Decisions
:::danger Common Misconception — Don't "Wait for Stable"!
A natural intuition is to treat trend states like a stock ticker:
- ❌ "It's **falling** → I'll wait until it reaches **stable** (the bottom)"
- ❌ "It's **rising** → too late, I missed the best price"
- ❌ "It's **stable** → now is the perfect time to act!"
**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 |
**"Rising" is NOT "too late" — it means NOW is the best time because prices will be higher later.**
:::
#### Basic Automation Pattern
For most appliances (dishwasher, washing machine, dryer), a single trend sensor is enough:
```yaml
# Example: Start dishwasher when prices are favorable
trigger:
- 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
action:
- 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 |
#### Dashboard Quick-Glance
On your dashboard, trend sensors give an instant overview:
- 🟢 All **falling/strongly_falling** → "Relax, prices are dropping — wait"
- 🔴 All **rising/strongly_rising** → "Start everything you can — it only gets more expensive"
- 🟡 **Mixed** → Compare short-term vs. long-term sensors, or check the Best Price Period sensor
### Trend Sensors vs Average Sensors
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 |
**Design principle:** Use **trend sensors** (enum) for visual feedback at a glance, use **average sensors** (numeric) for precise decision-making in automations.
### Configuration
Trend thresholds can be adjusted in the options flow:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** on your home
3. Navigate to **📈 Price Trend Thresholds**
4. Adjust the rising/falling and strongly rising/falling percentages
The thresholds are **volatility-adaptive**: on days with high price volatility, thresholds are widened automatically to prevent constant state changes. This means the trend sensors give more stable readings during volatile market conditions.
## Diagnostic Sensors
### Chart Metadata
**Entity ID:** `sensor.<home_name>_chart_metadata`
> **✨ New Feature**: This sensor provides dynamic chart configuration metadata for optimal visualization. Perfect for use with the `get_apexcharts_yaml` action!
This diagnostic sensor provides essential chart configuration values as sensor attributes, enabling dynamic Y-axis scaling and optimal chart appearance in rolling window modes.
**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)
**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
**Usage:**
The `tibber_prices.get_apexcharts_yaml` action **automatically uses this sensor** for dynamic Y-axis scaling in `rolling_window` and `rolling_window_autozoom` modes! No manual configuration needed - just enable the action's result with `config-template-card` and the sensor provides optimal Y-axis bounds automatically.
See the **[Chart Examples Guide](chart-examples.md)** for practical examples!
---
### Chart Data Export
**Entity ID:** `sensor.<home_name>_chart_data_export`
**Default State:** Disabled (must be manually enabled)
> **⚠️ Legacy Feature**: This sensor is maintained for backward compatibility. For new integrations, use the **`tibber_prices.get_chartdata`** service instead, which offers more flexibility and better performance.
This diagnostic sensor provides cached chart-friendly price data that can be consumed by chart cards (ApexCharts, custom cards, etc.).
**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)
**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
**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
**Configuration:**
To configure the sensor's output format:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** on your Tibber home
3. Navigate through the options wizard to **Step 7: Chart Data Export Settings**
4. Configure output format, filters, field names, and other options
5. Save and restart Home Assistant
**Available Settings:**
See the `tibber_prices.get_chartdata` service documentation below for a complete list of available parameters. All service parameters can be configured through the options flow.
**Example Usage:**
```yaml
# ApexCharts card consuming the sensor
type: custom:apexcharts-card
series:
- entity: sensor.<home_name>_chart_data_export
data_generator: |
return entity.attributes.data;
```
**Migration Path:**
If you're currently using this sensor, consider migrating to the service:
```yaml
# Old approach (sensor)
- service: apexcharts_card.update
data:
entity: sensor.<home_name>_chart_data_export
# New approach (service)
- service: tibber_prices.get_chartdata
data:
entry_id: YOUR_ENTRY_ID
day: ["today", "tomorrow"]
output_format: array_of_objects
response_variable: chart_data
```

View file

@ -0,0 +1,144 @@
---
comments: false
---
# Troubleshooting
## Common Issues
### Sensors Show "Unavailable"
**After initial setup or HA restart:**
This is normal. The integration needs up to one update cycle (15 minutes) to fetch data from the Tibber API. If sensors remain unavailable after 30 minutes:
1. Check your internet connection
2. Verify your Tibber API token is still valid at [developer.tibber.com](https://developer.tibber.com)
3. Check the logs for error messages (see [Debug Logging](#debug-logging) below)
**After working fine previously:**
- **API communication error**: Tibber's API may be temporarily down. The integration retries automatically — wait 1530 minutes.
- **Authentication expired**: If you see a "Reauth required" notification in HA, your API token needs to be re-entered. Go to **Settings → Devices & Services → Tibber Prices** and follow the reauth flow.
- **Rate limiting**: If you have multiple integrations using the same Tibber token, you may hit API rate limits. Check logs for "429" or "rate limit" messages.
### Tomorrow's Prices Not Available
Tomorrow's electricity prices are typically published by Tibber between **13:00 and 15:00 CET** (Central European Time). Before that time, all "tomorrow" sensors will show unavailable or their last known state.
The integration automatically polls more frequently in the afternoon to detect when tomorrow's data becomes available. No manual action is needed.
### Wrong Currency or Price Units
If prices show in the wrong currency or wrong unit (EUR vs ct):
1. Go to **Settings → Devices & Services → Tibber Prices → Configure**
2. Check the **Currency Display** step
3. Choose between base units (EUR, NOK, SEK) and sub-units (ct, øre)
Note: The currency is determined by your Tibber account's home country and cannot be changed — only the display unit (base vs. sub-unit) is configurable.
### No Best/Peak Price Periods Found
If the Best Price Period or Peak Price Period binary sensors never turn on:
1. **Check your flex settings**: A flex value that's too low may filter out all intervals. Try increasing it (e.g., from 10% to 20%).
2. **Enable relaxation**: In the options flow, enable relaxation for the affected period type. This automatically increases flex until periods are found.
3. **Check daily price variation**: On days with very flat prices (low volatility), periods may not meet the threshold criteria. This is expected behavior — the integration correctly identifies that no intervals stand out.
See the [Period Calculation Guide](period-calculation.md) for detailed configuration advice.
### Entities Duplicated After Reconfiguration
If you see duplicate entities after changing settings:
1. Go to **Settings → Devices & Services → Entities**
2. Filter by "Tibber Prices"
3. Remove any disabled or orphaned entities
4. Restart Home Assistant
### Integration Not Showing After Installation
If the integration doesn't appear in **Settings → Devices & Services → Add Integration**:
1. Confirm you restarted Home Assistant after installing via HACS
2. Clear your browser cache (Ctrl+Shift+R)
3. Check the HA logs for import errors related to `tibber_prices`
## Debug Logging
When reporting issues, debug logs help identify the problem quickly.
### Enable Debug Logging
Add this to your `configuration.yaml`:
```yaml
logger:
default: warning
logs:
custom_components.tibber_prices: debug
```
Restart Home Assistant for the change to take effect.
### Targeted Logging
For specific subsystems, you can enable logging selectively:
```yaml
logger:
default: warning
logs:
# API communication (requests, responses, errors)
custom_components.tibber_prices.api: debug
# Coordinator (data updates, caching, scheduling)
custom_components.tibber_prices.coordinator: debug
# Period calculation (best/peak price detection)
custom_components.tibber_prices.coordinator.period_handlers: debug
# Sensor value calculation
custom_components.tibber_prices.sensor: debug
```
### Temporary Debug Logging (No Restart)
You can also enable debug logging temporarily from the HA UI:
1. Go to **Developer Tools → Services**
2. Call service: `logger.set_level`
3. Data:
```yaml
custom_components.tibber_prices: debug
```
This resets when HA restarts.
### Downloading Diagnostics
For bug reports, include the integration's diagnostic dump:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click the three-dot menu (⋮) on the integration card
3. Select **Download diagnostics**
The downloaded file includes configuration, cache status, period information, and recent errors — with sensitive data redacted.
### What to Include in Bug Reports
When opening a [GitHub issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new):
1. **Integration version** (from Settings → Devices & Services → Tibber Prices)
2. **Home Assistant version** (from Settings → About)
3. **Description** of the problem and expected behavior
4. **Debug logs** (relevant excerpts from the HA log)
5. **Diagnostics file** (downloaded as described above)
6. **Steps to reproduce** (if applicable)
## Getting Help
- Check [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues)
- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) with detailed information
- Include logs, configuration, and steps to reproduce

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -0,0 +1,67 @@
{
"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
}
]
}

View file

@ -1,4 +1,5 @@
[
"v0.29.0",
"v0.28.0",
"v0.27.0",
"v0.24.0",

View file

@ -252,7 +252,7 @@ End after the Buy Me A Coffee button. No meta-commentary, no explanations."
# Use Claude Sonnet for better user-focused, plain-language release notes.
# Haiku tends to echo technical commit language; Sonnet better translates to user benefits.
# Can override with: COPILOT_MODEL=claude-haiku-4.5 ./scripts/release/generate-notes
COPILOT_MODEL="${COPILOT_MODEL:-claude-sonnet-4-6}"
COPILOT_MODEL="${COPILOT_MODEL:-claude-sonnet-4.6}"
# Call copilot CLI (it will handle authentication interactively)
copilot --model "$COPILOT_MODEL" < "$TEMP_PROMPT" || {
@ -344,6 +344,7 @@ EOF
fi
git-cliff --config "$CLIFF_CONFIG" "${FROM_TAG}..${TO_TAG}"
echo "" # Ensure output ends with newline (cliff.toml trim=true removes trailing newline)
if [ "$CLIFF_CONFIG" = "/tmp/cliff.toml" ]; then
rm -f /tmp/cliff.toml

View file

@ -102,7 +102,7 @@ def test_bug10_trend_diff_negative_current_price() -> None:
threshold_strongly_rising = 20.0
threshold_strongly_falling = -20.0
trend, diff_pct, trend_value = calculate_price_trend(
trend, diff_pct, trend_value, _ = calculate_price_trend(
current_interval_price=current_interval_price,
future_average=future_average,
threshold_rising=threshold_rising,
@ -135,7 +135,7 @@ def test_bug10_trend_diff_negative_falling_deeper() -> None:
threshold_strongly_rising = 20.0
threshold_strongly_falling = -20.0
trend, diff_pct, trend_value = calculate_price_trend(
trend, diff_pct, trend_value, _ = calculate_price_trend(
current_interval_price=current_interval_price,
future_average=future_average,
threshold_rising=threshold_rising,
@ -167,7 +167,7 @@ def test_bug10_trend_diff_zero_current_price() -> None:
threshold_strongly_rising = 20.0
threshold_strongly_falling = -20.0
trend, diff_pct, trend_value = calculate_price_trend(
trend, diff_pct, trend_value, _ = calculate_price_trend(
current_interval_price=current_interval_price,
future_average=future_average,
threshold_rising=threshold_rising,
@ -196,7 +196,7 @@ def test_bug10_trend_diff_positive_prices_unchanged() -> None:
threshold_strongly_rising = 20.0
threshold_strongly_falling = -20.0
trend, diff_pct, trend_value = calculate_price_trend(
trend, diff_pct, trend_value, _ = calculate_price_trend(
current_interval_price=current_interval_price,
future_average=future_average,
threshold_rising=threshold_rising,

View file

@ -448,7 +448,7 @@ def test_timer_group_sizes() -> None:
"""
# As of Nov 2025
expected_time_sensitive_min = 40 # At least 40 sensors
expected_minute_update = 6 # Exactly 6 timing sensors
expected_minute_update = 7 # Exactly 7 timing sensors
assert len(TIME_SENSITIVE_ENTITY_KEYS) >= expected_time_sensitive_min, (
f"Expected at least {expected_time_sensitive_min} TIME_SENSITIVE sensors, got {len(TIME_SENSITIVE_ENTITY_KEYS)}"