mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-07-27 17:26:48 +00:00
Compare commits
13 commits
cd59834277
...
8aa5769784
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aa5769784 | ||
|
|
c7af02f7c2 | ||
|
|
2f704a35a3 | ||
|
|
d6bd933e90 | ||
|
|
07117801d2 | ||
|
|
1db86d1766 | ||
|
|
c610dbe1a3 | ||
|
|
ac7cd5b572 | ||
|
|
f2f0d296d1 | ||
|
|
cbbfadbf4f | ||
|
|
c494d0e39d | ||
|
|
c892d7376c | ||
|
|
0e699ae142 |
59 changed files with 3577 additions and 1902 deletions
|
|
@ -134,11 +134,32 @@
|
|||
"version": "latest",
|
||||
"profile": "minimal"
|
||||
},
|
||||
"ghcr.io/devcontainer-community/devcontainer-features/yq:1": {
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
|
||||
"packages": [
|
||||
"bat",
|
||||
"eza",
|
||||
"fd-find",
|
||||
"ffmpeg",
|
||||
"fzf",
|
||||
"git-delta",
|
||||
"httpie",
|
||||
"hyperfine",
|
||||
"ipython3",
|
||||
"jo",
|
||||
"jq",
|
||||
"libpcap-dev",
|
||||
"libturbojpeg0",
|
||||
"libpcap-dev"
|
||||
"miller",
|
||||
"moreutils",
|
||||
"ripgrep",
|
||||
"shellcheck",
|
||||
"shfmt",
|
||||
"sqlite3",
|
||||
"tree",
|
||||
"yamllint"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
.github/workflows/docusaurus.yml
vendored
9
.github/workflows/docusaurus.yml
vendored
|
|
@ -52,6 +52,15 @@ jobs:
|
|||
docs/user/package-lock.json
|
||||
docs/developer/package-lock.json
|
||||
|
||||
# VERIFY GENERATED DOCS
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Verify sensor reference is up-to-date
|
||||
run: python3 scripts/docs/generate-sensor-reference --check
|
||||
|
||||
# USER DOCS BUILD
|
||||
- name: Install user docs dependencies
|
||||
working-directory: docs/user
|
||||
|
|
|
|||
21
AGENTS.md
21
AGENTS.md
|
|
@ -783,6 +783,14 @@ When debugging period calculation issues:
|
|||
- **git-cliff**: Template-based release notes (fast, reliable, installed via cargo in `scripts/setup/setup`)
|
||||
- Manual grep/awk parsing as fallback (always available)
|
||||
|
||||
**Agent Productivity CLI Tools (DevContainer):**
|
||||
|
||||
The devcontainer provides common agent-facing CLI tools: `bat`, `delta`/`git-delta`, `eza`, `fd`/`fdfind`, `fzf`, `http`/`httpie`, `hyperfine`, `ipython`, `jq`, `jo`, `mlr`/`miller`, `rg`/`ripgrep`, `shellcheck`, `shfmt`, `sponge`, `sqlite3`, `tree`, `yq`, and `yamllint`. Prefer these explicit container tools over assuming a VS Code extension exposes an equivalent CLI on `PATH`.
|
||||
|
||||
**CLI Compatibility Notes:**
|
||||
|
||||
Some commands are available via compatibility aliases because Debian package names differ from what agents often expect. Prefer these stable spellings: `bat`, `fd`, `git-delta`, `http`, `ipython`, `miller`, and `ripgrep`. `yq` is installed as the Mike Farah variant, so standard `yq eval`/`yq e` syntax is expected. Setup creates compatibility symlinks automatically in `scripts/setup/setup`.
|
||||
|
||||
**When generating shell commands:**
|
||||
|
||||
1. **Prefer development scripts** (they handle .venv automatically)
|
||||
|
|
@ -843,7 +851,14 @@ If you notice commands failing or missing dependencies:
|
|||
./scripts/type-check # Run Pyright type checking
|
||||
./scripts/lint-check # Run Ruff linting (check-only, CI mode)
|
||||
./scripts/lint # Run Ruff linting with auto-fix
|
||||
./scripts/check # Run both type-check + lint-check (recommended before commits)
|
||||
./scripts/check # Run both type-check + lint-check + sensor reference freshness (recommended before commits)
|
||||
```
|
||||
|
||||
**Documentation generation:**
|
||||
|
||||
```bash
|
||||
./scripts/docs/generate-sensor-reference # Regenerate sensor-reference.md from translation files
|
||||
./scripts/docs/generate-sensor-reference --check # Verify reference is current (used in CI)
|
||||
```
|
||||
|
||||
**Local validation:**
|
||||
|
|
@ -2098,6 +2113,8 @@ Public entry points → direct helpers (call order) → pure utilities. Prefix p
|
|||
- **UI translations**: Multi-language support exists in `/translations/` and `/custom_translations/` (de, en, nb, nl, sv) for UI strings only
|
||||
- **Why English-only docs**: Ensures maintainability, accessibility to global community, and consistency with Home Assistant ecosystem
|
||||
- **Entity names in documentation**: Use **translated display names** from `/translations/en.json` (what users see), not internal entity IDs. Example: "Best Price Period" not "sensor.tibber_home_best_price_period" (add entity ID as comment if needed for clarity).
|
||||
- **Entity reference annotations**: When mentioning entity display names in docs, add the `translation_key` (= entity ID suffix) on first mention per section: `**Display Name** (\`translation_key\`)`. This helps users find entities regardless of UI language. See `docs/user/docs/sensor-reference.md` for the auto-generated multi-language lookup table.
|
||||
- **Entity ID tip boxes**: All doc pages with entity ID examples should include the standardized tip box linking to `sensor-reference.md`. Use the same wording as in existing pages (search for "Entity ID tip" for the template).
|
||||
|
||||
**Examples and use cases:**
|
||||
|
||||
|
|
@ -2708,6 +2725,8 @@ After the sensor.py refactoring (completed Nov 2025), sensors are organized by *
|
|||
- **Volatility**: Statistical analysis of price variation
|
||||
- **Diagnostic**: System information and metadata
|
||||
|
||||
**IMPORTANT — After adding/renaming entities**: Run `./scripts/docs/generate-sensor-reference` to regenerate the multi-language sensor reference table. The `scripts/check` and CI will fail if the reference is stale.
|
||||
|
||||
2. **Add entity description** to appropriate sensor group in `sensor/definitions.py`:
|
||||
|
||||
- `INTERVAL_PRICE_SENSORS`, `INTERVAL_LEVEL_SENSORS`, or `INTERVAL_RATING_SENSORS`
|
||||
|
|
|
|||
381
README.md
381
README.md
|
|
@ -16,367 +16,179 @@
|
|||
> **⚠️ Not affiliated with Tibber**
|
||||
> This is an independent, community-maintained custom integration for Home Assistant. It is **not** an official Tibber product and is **not** affiliated with or endorsed by Tibber AS.
|
||||
|
||||
A custom Home Assistant integration that provides advanced electricity price information and ratings from Tibber. This integration fetches **quarter-hourly** electricity prices, enriches them with statistical analysis, and provides smart indicators to help you optimize your energy consumption and save money.
|
||||
**The most comprehensive Tibber price integration for Home Assistant.** Get 100+ sensors with quarter-hourly precision, intelligent best/peak price period detection, price forecasts, trend analysis, volatility tracking, and beautiful chart visualizations - all from a single integration. Automate your energy consumption like a pro.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
**[📚 Complete Documentation](https://jpawlowski.github.io/hass.tibber_prices/)** - Two comprehensive documentation sites:
|
||||
**[📚 Complete Documentation](https://jpawlowski.github.io/hass.tibber_prices/)** — Installation, guides, examples, and full sensor reference:
|
||||
|
||||
- **[👤 User Documentation](https://jpawlowski.github.io/hass.tibber_prices/user/)** - Installation, configuration, usage guides, and examples
|
||||
- **[🔧 Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/)** - Architecture, contributing guidelines, and development setup
|
||||
- **[👤 User Documentation](https://jpawlowski.github.io/hass.tibber_prices/user/)** — Setup, sensors, automations, dashboards
|
||||
- **[🔧 Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/)** — Architecture, contributing, development
|
||||
|
||||
**Quick Links:**
|
||||
- [Installation Guide](https://jpawlowski.github.io/hass.tibber_prices/user/installation) - Step-by-step setup instructions
|
||||
- [Sensor Reference](https://jpawlowski.github.io/hass.tibber_prices/user/sensors) - Complete list of all sensors and attributes
|
||||
- [Chart Examples](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples) - ApexCharts visualizations
|
||||
- [Automation Examples](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples) - Real-world automation scenarios
|
||||
- [Changelog](https://github.com/jpawlowski/hass.tibber_prices/releases) - Release history and notes
|
||||
[Installation](https://jpawlowski.github.io/hass.tibber_prices/user/installation) · [Sensor Reference](https://jpawlowski.github.io/hass.tibber_prices/user/sensor-reference) · [Charts](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples) · [Automations](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples) · [FAQ](https://jpawlowski.github.io/hass.tibber_prices/user/faq) · [Changelog](https://github.com/jpawlowski/hass.tibber_prices/releases)
|
||||
|
||||
## ✨ Features
|
||||
## ✨ Why This Integration?
|
||||
|
||||
- **Quarter-Hourly Price Data**: Access detailed 15-minute interval pricing (384 data points across 4 days: day before yesterday/yesterday/today/tomorrow)
|
||||
- **Flexible Currency Display**: Choose between base currency (€, kr) or subunit (ct, øre) display - configurable per your preference with smart defaults
|
||||
- **Multi-Currency Support**: Automatic detection and formatting for EUR, NOK, SEK, DKK, USD, and GBP
|
||||
- **Price Level Indicators**: Know when you're in a VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, or VERY_EXPENSIVE period
|
||||
- **Statistical Sensors**: Track lowest, highest, and average prices for the day
|
||||
- **Price Ratings**: Quarter-hourly ratings comparing current prices to 24-hour trailing averages
|
||||
- **Smart Indicators**: Binary sensors to detect peak hours and best price hours for automations
|
||||
- **Beautiful ApexCharts**: Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples))
|
||||
- **Chart Metadata Sensor**: Dynamic chart configuration for optimal visualization
|
||||
- **Intelligent Caching**: Minimizes API calls while ensuring data freshness across Home Assistant restarts
|
||||
- **Custom Actions** (backend services): API endpoints for advanced integrations (ApexCharts support included)
|
||||
- **Diagnostic Sensors**: Monitor data freshness and availability
|
||||
- **Reliable API Usage**: Uses only official Tibber [`priceInfo`](https://developer.tibber.com/docs/reference#priceinfo) and [`priceInfoRange`](https://developer.tibber.com/docs/reference#subscription) endpoints - no legacy APIs. Price ratings and statistics are calculated locally for maximum reliability and future-proofing.
|
||||
Most Tibber integrations give you a single price sensor. This one gives you a **complete energy optimization toolkit**:
|
||||
|
||||
### 🔮 Know What's Coming
|
||||
|
||||
- **Quarter-hourly precision** — 15-minute interval prices, not just hourly averages
|
||||
- **Price forecasts** — See average prices for the next 1h, 2h, 3h, ... up to 12h ahead
|
||||
- **Trend analysis** — Know if prices are rising, falling, or stable — and when the next trend change happens
|
||||
- **Price trajectory** — Detect turning points before they happen (first-half vs second-half window comparison)
|
||||
- **Price outlook** — Instantly see if the next hours will be cheaper or more expensive than now
|
||||
|
||||
### ⚡ Automate Smartly
|
||||
|
||||
- **Best Price & Peak Price Periods** — Intelligent binary sensors that detect the cheapest and most expensive periods of the day, with configurable flexibility, relaxation strategies, and gap tolerance ([how it works](https://jpawlowski.github.io/hass.tibber_prices/user/period-calculation))
|
||||
- **Period timing sensors** — Duration, end time, remaining minutes, progress percentage, and countdown to next period — everything you need for advanced automations
|
||||
- **Runtime configuration** — Adjust period detection parameters on the fly via switches and number entities, without restarting — perfect for automations that adapt to your schedule
|
||||
- **5-level price classification** — VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE from Tibber's API
|
||||
- **3-level price ratings** — LOW, NORMAL, HIGH based on 24h trailing average comparison
|
||||
|
||||
### 📊 Visualize Beautifully
|
||||
|
||||
- **Auto-generated ApexCharts** — One action call generates a complete chart configuration with dynamic Y-axis scaling and color-coded price levels ([see examples](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples))
|
||||
- **Dynamic icons & colors** — Every sensor adapts its icon and color to the current price state — cheap prices glow green, expensive ones turn red ([icon guide](https://jpawlowski.github.io/hass.tibber_prices/user/dynamic-icons))
|
||||
- **Chart data export** — Flexible data API with filtering, resolution control, and multiple output formats for any visualization card
|
||||
|
||||
### 📈 Understand Your Market
|
||||
|
||||
- **Volatility analysis** — Know if today's prices are stable or wild (low/moderate/high/very_high)
|
||||
- **Daily & rolling statistics** — Min, max, average, median for today, tomorrow, trailing 24h, and leading 24h
|
||||
- **Energy & tax breakdown** — See spot price vs. tax components as sensor attributes
|
||||
- **Multi-currency support** — EUR, NOK, SEK, DKK, USD, GBP with configurable base/subunit display (€ vs ct, kr vs øre)
|
||||
|
||||
### 🛡️ Built for Reliability
|
||||
|
||||
- **Intelligent caching** — Multi-layer caching minimizes API calls, survives HA restarts, auto-invalidates at midnight
|
||||
- **High-performance interval pool** — O(1) timestamp lookups, gap detection, auto-fetching of missing data
|
||||
- **Quarter-hour precision updates** — Sensors refresh at :00/:15/:30/:45 boundaries, independent of API polling
|
||||
- **Official API only** — Uses Tibber's [`priceInfo`](https://developer.tibber.com/docs/reference#priceinfo) and [`priceInfoRange`](https://developer.tibber.com/docs/reference#subscription) endpoints. All ratings and statistics are calculated locally.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Step 1: Install the Integration
|
||||
### Step 1: Install via HACS
|
||||
|
||||
**Prerequisites:** This integration requires [HACS](https://hacs.xyz/) (Home Assistant Community Store) to be installed.
|
||||
|
||||
Click the button below to open the integration directly in HACS:
|
||||
**Prerequisites:** [HACS](https://hacs.xyz/) (Home Assistant Community Store) must be installed.
|
||||
|
||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
|
||||
|
||||
Then:
|
||||
1. Click "Download" to install
|
||||
2. **Restart Home Assistant**
|
||||
|
||||
1. Click "Download" to install the integration
|
||||
2. **Restart Home Assistant** (required after installation)
|
||||
|
||||
> **Note:** The My Home Assistant redirect will first take you to a landing page. Click the button there to open your Home Assistant instance. If the repository is not yet in the HACS default store, HACS will ask if you want to add it as a custom repository.
|
||||
|
||||
### Step 2: Add and Configure the Integration
|
||||
|
||||
**Important:** You must have installed the integration first (see Step 1) and restarted Home Assistant!
|
||||
|
||||
#### Option 1: One-Click Setup (Quick)
|
||||
|
||||
Click the button below to open the configuration dialog:
|
||||
### Step 2: Configure
|
||||
|
||||
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=tibber_prices)
|
||||
|
||||
This will guide you through:
|
||||
|
||||
1. Enter your Tibber API token ([get one here](https://developer.tibber.com/settings/access-token))
|
||||
2. Select your Tibber home
|
||||
3. Configure price thresholds (optional)
|
||||
3. Configure price thresholds (optional — sensible defaults are provided)
|
||||
|
||||
#### Option 2: Manual Configuration
|
||||
Or manually: **Settings** → **Devices & Services** → **+ Add Integration** → search "Tibber Price Information & Ratings"
|
||||
|
||||
1. Go to **Settings** → **Devices & Services**
|
||||
2. Click **"+ Add Integration"**
|
||||
3. Search for "Tibber Price Information & Ratings"
|
||||
4. Follow the configuration steps (same as Option 1)
|
||||
### Step 3: Done!
|
||||
|
||||
### Step 3: Start Using!
|
||||
|
||||
- 30+ sensors are now available (key sensors enabled by default)
|
||||
- Configure additional sensors in **Settings** → **Devices & Services** → **Tibber Price Information & Ratings** → **Entities**
|
||||
- Use sensors in automations, dashboards, and scripts
|
||||
- **100+ sensors** are now available (key sensors enabled by default, advanced ones ready to enable)
|
||||
- Explore entities in **Settings** → **Devices & Services** → **Tibber Price Information & Ratings**
|
||||
- Start building automations, dashboards, and energy-saving workflows
|
||||
|
||||
📖 **[Full Installation Guide →](https://jpawlowski.github.io/hass.tibber_prices/user/installation)**
|
||||
|
||||
## 📊 Available Entities
|
||||
## 📊 What You Get
|
||||
|
||||
The integration provides **30+ sensors** across different categories. Key sensors are enabled by default, while advanced sensors can be enabled as needed.
|
||||
The integration provides **100+ entities** across sensors, binary sensors, switches, and number entities. Here are the highlights — all key sensors are **enabled by default**:
|
||||
|
||||
> **Rich Sensor Attributes**: All sensors include extensive attributes with timestamps, context data, and detailed explanations. Enable **Extended Descriptions** in the integration options to add `long_description` and `usage_tips` attributes to every sensor, providing in-context documentation directly in Home Assistant's UI.
|
||||
<img src="https://raw.githubusercontent.com/jpawlowski/hass.tibber_prices/main/docs/user/static/img/entities-overview.jpg" width="400" alt="Entity list showing dynamic icons for different price states">
|
||||
|
||||
**[📋 Complete Sensor Reference](https://jpawlowski.github.io/hass.tibber_prices/user/sensors)** - Full list with descriptions and attributes
|
||||
| Category | Highlights | Count |
|
||||
|----------|-----------|-------|
|
||||
| **💰 Prices** | Current, next & previous interval price + rolling hour averages | 6+ |
|
||||
| **📊 Statistics** | Daily min/max/avg for today & tomorrow, 24h trailing & leading windows | 12+ |
|
||||
| **🔮 Forecasts** | Next 1h–12h average prices, price outlook & trajectory sensors | 20+ |
|
||||
| **📈 Trends** | Current trend direction, next trend change time & countdown | 3 |
|
||||
| **📉 Volatility** | Today, tomorrow, next 24h & combined volatility levels | 4 |
|
||||
| **🏷️ Levels & Ratings** | 5-level (API) and 3-level (computed) classification per interval, hour & day | 12+ |
|
||||
| **⏰ Period Timing** | Best/peak: end time, duration, remaining, progress, next start | 10+ |
|
||||
| **🔌 Binary Sensors** | Best price period, peak price period, tomorrow data available, API connection | 4+ |
|
||||
| **🎛️ Runtime Config** | Switches & numbers to adjust period detection live — no restart needed | 14 |
|
||||
| **🔧 Diagnostics** | Data lifecycle status, home metadata, grid info, subscription status | 15+ |
|
||||
|
||||
### Core Price Sensors (Enabled by Default)
|
||||
> **Every sensor includes rich attributes** — timestamps, detailed descriptions, and context data. Enable **Extended Descriptions** in the integration options to get `long_description` and `usage_tips` on every entity.
|
||||
|
||||
| Entity | Description |
|
||||
| -------------------------- | ------------------------------------------------- |
|
||||
| Current Electricity Price | Current 15-minute interval price |
|
||||
| Next Interval Price | Price for the next 15-minute interval |
|
||||
| Current Hour Average Price | Average of current hour's 4 intervals |
|
||||
| Next Hour Average Price | Average of next hour's 4 intervals |
|
||||
| Current Price Level | API classification (VERY_CHEAP to VERY_EXPENSIVE) |
|
||||
| Next Interval Price Level | Price level for next interval |
|
||||
| Current Hour Price Level | Price level for current hour average |
|
||||
| Next Hour Price Level | Price level for next hour average |
|
||||
📖 **[Complete Sensor Reference →](https://jpawlowski.github.io/hass.tibber_prices/user/sensor-reference)** — All entities with descriptions, attributes, and multi-language lookup
|
||||
|
||||
### Statistical Sensors (Enabled by Default)
|
||||
## 🤖 Automation Sneak Peek
|
||||
|
||||
| Entity | Description |
|
||||
| ------------------------- | ------------------------------------------- |
|
||||
| Today's Lowest Price | Minimum price for today |
|
||||
| Today's Highest Price | Maximum price for today |
|
||||
| Today's Average Price | Mean price across today's intervals |
|
||||
| Tomorrow's Lowest Price | Minimum price for tomorrow (when available) |
|
||||
| Tomorrow's Highest Price | Maximum price for tomorrow (when available) |
|
||||
| Tomorrow's Average Price | Mean price for tomorrow (when available) |
|
||||
| Leading 24h Average Price | Average of next 24 hours from now |
|
||||
| Leading 24h Minimum Price | Lowest price in next 24 hours |
|
||||
| Leading 24h Maximum Price | Highest price in next 24 hours |
|
||||
> See the **[full automation examples guide](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples)** for more recipes.
|
||||
|
||||
### Price Rating Sensors (Enabled by Default)
|
||||
|
||||
| Entity | Description |
|
||||
| -------------------------- | --------------------------------------------------------- |
|
||||
| Current Price Rating | % difference from 24h trailing average (current interval) |
|
||||
| Next Interval Price Rating | % difference from 24h trailing average (next interval) |
|
||||
| Current Hour Price Rating | % difference for current hour average |
|
||||
| Next Hour Price Rating | % difference for next hour average |
|
||||
|
||||
> **How ratings work**: Compares each interval to the average of the previous 96 intervals (24 hours). Positive values mean prices are above average, negative means below average.
|
||||
|
||||
### Binary Sensors (Enabled by Default)
|
||||
|
||||
| Entity | Description |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Peak Price Period | ON when in a detected peak price period ([how it works](https://jpawlowski.github.io/hass.tibber_prices/user/period-calculation)) |
|
||||
| Best Price Period | ON when in a detected best price period ([how it works](https://jpawlowski.github.io/hass.tibber_prices/user/period-calculation)) |
|
||||
| Tibber API Connection | Connection status to Tibber API |
|
||||
| Tomorrow's Data Available | Whether tomorrow's price data is available |
|
||||
|
||||
### Diagnostic Sensors (Enabled by Default)
|
||||
|
||||
| Entity | Description |
|
||||
| --------------- | ------------------------------------------ |
|
||||
| Data Expiration | Timestamp when current data expires |
|
||||
| Price Forecast | Formatted list of upcoming price intervals |
|
||||
|
||||
### Additional Sensors (Disabled by Default)
|
||||
|
||||
The following sensors are available but disabled by default. Enable them in `Settings > Devices & Services > Tibber Price Information & Ratings > Entities`:
|
||||
|
||||
- **Previous Interval Price** & **Previous Interval Price Level**: Historical data for the last 15-minute interval
|
||||
- **Previous Interval Price Rating**: Rating for the previous interval
|
||||
- **Trailing 24h Average Price**: Average of the past 24 hours from now
|
||||
- **Trailing 24h Minimum/Maximum Price**: Min/max in the past 24 hours
|
||||
|
||||
> **Note**: Currency display is configurable during setup. Choose between:
|
||||
> - **Base currency** (€/kWh, kr/kWh) - decimal values, differences visible from 3rd-4th decimal
|
||||
> - **Subunit** (ct/kWh, øre/kWh) - larger values, differences visible from 1st decimal
|
||||
>
|
||||
> Smart defaults: EUR → subunit (German/Dutch preference), NOK/SEK/DKK → base (Scandinavian preference). Supported currencies: EUR, NOK, SEK, DKK, USD, GBP.
|
||||
|
||||
## Automation Examples> **Note:** See the [full automation examples guide](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples) for more advanced recipes.
|
||||
|
||||
### Run Appliances During Cheap Hours
|
||||
|
||||
Use the `binary_sensor.tibber_best_price_period` to automatically start appliances during detected best price periods:
|
||||
**Run appliances when electricity is cheapest:**
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: "Run Dishwasher During Cheap Hours"
|
||||
- alias: "Start Dishwasher During Best Price Period"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: binary_sensor.tibber_best_price_period
|
||||
to: "on"
|
||||
condition:
|
||||
- condition: time
|
||||
after: "21:00:00"
|
||||
before: "06:00:00"
|
||||
action:
|
||||
- service: switch.turn_on
|
||||
- action: switch.turn_on
|
||||
target:
|
||||
entity_id: switch.dishwasher
|
||||
```
|
||||
|
||||
> **Learn more:** The [period calculation guide](https://jpawlowski.github.io/hass.tibber_prices/user/period-calculation) explains how Best/Peak Price periods are identified and how you can configure filters (flexibility, minimum distance from average, price level filters with gap tolerance).
|
||||
|
||||
### Notify on Extremely High Prices
|
||||
|
||||
Get notified when prices reach the VERY_EXPENSIVE level:
|
||||
**Reduce heating when prices spike above average:**
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: "Notify on Very Expensive Electricity"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: sensor.tibber_current_interval_price_level
|
||||
to: "VERY_EXPENSIVE"
|
||||
action:
|
||||
- service: notify.mobile_app
|
||||
data:
|
||||
title: "⚠️ High Electricity Prices"
|
||||
message: "Current electricity price is in the VERY EXPENSIVE range. Consider reducing consumption."
|
||||
```
|
||||
|
||||
### Temperature Control Based on Price Ratings
|
||||
|
||||
Adjust heating/cooling when current prices are significantly above the 24h average:
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: "Reduce Heating During High Price Ratings"
|
||||
- alias: "Reduce Heating During High Prices"
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.tibber_current_interval_price_rating
|
||||
above: 20 # More than 20% above 24h average
|
||||
action:
|
||||
- service: climate.set_temperature
|
||||
- action: climate.set_temperature
|
||||
target:
|
||||
entity_id: climate.living_room
|
||||
data:
|
||||
temperature: 19 # Lower target temperature
|
||||
temperature: 19
|
||||
```
|
||||
|
||||
### Smart EV Charging Based on Tomorrow's Prices
|
||||
📖 **[More automations →](https://jpawlowski.github.io/hass.tibber_prices/user/automation-examples)** — EV charging, heat pump control, price notifications, and more
|
||||
|
||||
Start charging when tomorrow's prices drop below today's average:
|
||||
## 📈 Chart Visualizations
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: "Smart EV Charging"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: binary_sensor.tibber_best_price_interval
|
||||
to: "on"
|
||||
condition:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.tibber_current_interval_price_rating
|
||||
below: -15 # At least 15% below average
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.ev_battery_level
|
||||
below: 80
|
||||
action:
|
||||
- service: switch.turn_on
|
||||
target:
|
||||
entity_id: switch.ev_charger
|
||||
```
|
||||
Generate beautiful price charts with a single action call — dynamic Y-axis, color-coded price levels, and multiple chart modes included.
|
||||
|
||||
## Troubleshooting
|
||||
<img src="https://raw.githubusercontent.com/jpawlowski/hass.tibber_prices/main/docs/user/static/img/charts/rolling-window.jpg" width="600" alt="Dynamic 48h rolling window chart with color-coded price levels">
|
||||
|
||||
### No data appearing
|
||||
📖 **[Chart examples & setup →](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples)** | **[Actions reference →](https://jpawlowski.github.io/hass.tibber_prices/user/actions)**
|
||||
|
||||
1. Check your API token is valid at [developer.tibber.com](https://developer.tibber.com/settings/access-token)
|
||||
2. Verify you have an active Tibber subscription
|
||||
3. Check the Home Assistant logs for detailed error messages (`Settings > System > Logs`)
|
||||
4. Restart the integration: `Settings > Devices & Services > Tibber Price Information & Ratings > ⋮ > Reload`
|
||||
## ❓ Help & Support
|
||||
|
||||
### Missing tomorrow's price data
|
||||
|
||||
- Tomorrow's price data typically becomes available between **13:00 and 15:00** each day (Nordic time)
|
||||
- The integration automatically checks more frequently during this window
|
||||
- Check `binary_sensor.tibber_tomorrows_data_available` to see if data is available
|
||||
- If data is unavailable after 15:00, verify it's available in the Tibber app first
|
||||
|
||||
### Prices not updating at quarter-hour boundaries
|
||||
|
||||
- Entities automatically refresh at 00/15/30/45-minute marks without waiting for API polls
|
||||
- Check `sensor.tibber_data_expiration` to verify data freshness
|
||||
- The integration caches data intelligently and survives Home Assistant restarts
|
||||
|
||||
### Currency or units showing incorrectly
|
||||
|
||||
- Currency is automatically detected from your Tibber account
|
||||
- Display mode (base currency vs. subunit) can be configured in integration options: `Settings > Devices & Services > Tibber Price Information & Ratings > Configure`
|
||||
- Supported currencies: EUR, NOK, SEK, DKK, USD, and GBP
|
||||
- Smart defaults apply: EUR users get subunit (ct), Scandinavian users get base currency (kr)
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Sensor Attributes
|
||||
|
||||
Every sensor includes rich attributes beyond just the state value. These attributes provide context, timestamps, and additional data useful for automations and templates.
|
||||
|
||||
**Standard attributes available on most sensors:**
|
||||
|
||||
- `timestamp` - ISO 8601 timestamp for the data point
|
||||
- `description` - Brief explanation of what the sensor represents
|
||||
- `level_id` and `level_value` - For price level sensors (e.g., `VERY_CHEAP` = -2)
|
||||
|
||||
**Extended descriptions** (enable in integration options):
|
||||
|
||||
- `long_description` - Detailed explanation of the sensor's purpose
|
||||
- `usage_tips` - Practical suggestions for using the sensor in automations
|
||||
|
||||
**Example - Current Price sensor attributes:**
|
||||
|
||||
```yaml
|
||||
timestamp: "2025-11-03T14:15:00+01:00"
|
||||
description: "The current electricity price per kWh"
|
||||
long_description: "Shows the current price per kWh from your Tibber subscription"
|
||||
usage_tips: "Use this to track prices or to create automations that run when electricity is cheap"
|
||||
```
|
||||
|
||||
**Example template using attributes:**
|
||||
|
||||
```yaml
|
||||
template:
|
||||
- sensor:
|
||||
- name: "Price Status"
|
||||
state: >
|
||||
{% set price = states('sensor.tibber_current_electricity_price') | float %}
|
||||
{% set timestamp = state_attr('sensor.tibber_current_electricity_price', 'timestamp') %}
|
||||
Price at {{ timestamp }}: {{ price }} ct/kWh
|
||||
```
|
||||
|
||||
📖 **[View all sensors and attributes →](https://jpawlowski.github.io/hass.tibber_prices/user/sensors)**
|
||||
|
||||
### Dynamic Icons & Visual Indicators
|
||||
|
||||
All sensors feature dynamic icons that change based on price levels, providing instant visual feedback in your dashboards.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/jpawlowski/hass.tibber_prices/main/docs/user/static/img/entities-overview.jpg" width="400" alt="Entity list showing dynamic icons for different price states">
|
||||
|
||||
_Dynamic icons adapt to price levels, trends, and period states - showing CHEAP prices, FALLING trend, and active Best Price Period_
|
||||
|
||||
📖 **[Dynamic Icons Guide →](https://jpawlowski.github.io/hass.tibber_prices/user/dynamic-icons)** | **[Icon Colors Guide →](https://jpawlowski.github.io/hass.tibber_prices/user/icon-colors)**
|
||||
|
||||
### Custom Actions
|
||||
|
||||
The integration provides custom actions (they still appear as services under the hood) for advanced use cases. These actions show up in Home Assistant under **Developer Tools → Actions**.
|
||||
|
||||
- `tibber_prices.get_chartdata` - Get price data in chart-friendly formats for any visualization card
|
||||
- `tibber_prices.get_apexcharts_yaml` - Generate complete ApexCharts configurations
|
||||
- `tibber_prices.refresh_user_data` - Manually refresh account information
|
||||
|
||||
📖 **[Action documentation and examples →](https://jpawlowski.github.io/hass.tibber_prices/user/actions)**
|
||||
|
||||
### Chart Visualizations (Optional)
|
||||
|
||||
The integration includes built-in support for creating price visualization cards with automatic Y-axis scaling and color-coded series.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/jpawlowski/hass.tibber_prices/main/docs/user/static/img/charts/rolling-window.jpg" width="600" alt="Example: Dynamic 48h rolling window chart">
|
||||
|
||||
_Optional: Dynamic 48h chart with automatic Y-axis scaling - generated via `get_apexcharts_yaml` action_
|
||||
|
||||
📖 **[Chart examples and setup guide →](https://jpawlowski.github.io/hass.tibber_prices/user/chart-examples)**
|
||||
- 📖 **[FAQ](https://jpawlowski.github.io/hass.tibber_prices/user/faq)** — Common questions answered
|
||||
- 🔧 **[Troubleshooting](https://jpawlowski.github.io/hass.tibber_prices/user/troubleshooting)** — Solving common issues
|
||||
- 🐛 **[Report an Issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new)** — Found a bug? Let us know
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please read the [Contributing Guidelines](CONTRIBUTING.md) and [Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/) before submitting pull requests.
|
||||
Contributions are welcome! See the [Contributing Guidelines](CONTRIBUTING.md) and [Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/) to get started.
|
||||
|
||||
### For Contributors
|
||||
|
||||
- **[Developer Setup](https://jpawlowski.github.io/hass.tibber_prices/developer/setup)** - Get started with DevContainer
|
||||
- **[Architecture Guide](https://jpawlowski.github.io/hass.tibber_prices/developer/architecture)** - Understand the codebase
|
||||
- **[Release Management](https://jpawlowski.github.io/hass.tibber_prices/developer/release-management)** - Release process and versioning
|
||||
- **[Developer Setup](https://jpawlowski.github.io/hass.tibber_prices/developer/setup)** — DevContainer-based development environment
|
||||
- **[Architecture Guide](https://jpawlowski.github.io/hass.tibber_prices/developer/architecture)** — Understand the codebase
|
||||
- **[Release Management](https://jpawlowski.github.io/hass.tibber_prices/developer/release-management)** — Release process and versioning
|
||||
|
||||
## 🤖 Development Note
|
||||
|
||||
This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). While AI enables rapid development and helps implement complex features, it's possible that some edge cases or subtle bugs may exist that haven't been discovered yet. If you encounter any issues, please [open an issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) - we'll work on fixing them (with AI help, of course! 😊).
|
||||
This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). While AI enables rapid development, it's possible that some edge cases haven't been discovered yet. If you encounter any issues, please [open an issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) — we'll fix them (with AI help, of course! 😊).
|
||||
|
||||
The integration is actively maintained and benefits from AI's ability to quickly understand and implement Home Assistant patterns, maintain consistency across the codebase, and handle complex data transformations. Quality is ensured through automated linting (Ruff), Home Assistant's type checking, and real-world testing.
|
||||
Quality is ensured through automated linting (Ruff), static type checking (Pyright), and real-world testing.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -386,7 +198,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||
[commits]: https://github.com/jpawlowski/hass.tibber_prices/commits/main
|
||||
[hacs]: https://github.com/hacs/integration
|
||||
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
|
||||
[exampleimg]: https://raw.githubusercontent.com/jpawlowski/hass.tibber_prices/main/images/example.png
|
||||
[license-shield]: https://img.shields.io/github/license/jpawlowski/hass.tibber_prices.svg?style=for-the-badge
|
||||
[maintenance-shield]: https://img.shields.io/badge/maintainer-%40jpawlowski-blue.svg?style=for-the-badge
|
||||
[user_profile]: https://github.com/jpawlowski
|
||||
|
|
|
|||
|
|
@ -207,11 +207,8 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
|
|||
# Get expected intervals for tomorrow (handles DST)
|
||||
expected_intervals = self.coordinator.time.get_expected_intervals_for_day(tomorrow_date)
|
||||
|
||||
if interval_count == expected_intervals:
|
||||
return True
|
||||
if interval_count == 0:
|
||||
return False
|
||||
return False
|
||||
# True only when ALL intervals are available (partial = not available)
|
||||
return interval_count == expected_intervals
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
valid_to_dt = datetime.fromisoformat(valid_to)
|
||||
if valid_to_dt < datetime.now(valid_to_dt.tzinfo):
|
||||
return "expired"
|
||||
except (ValueError, AttributeError):
|
||||
except ValueError, AttributeError:
|
||||
pass # If parsing fails, continue with other checks
|
||||
|
||||
# Check validFrom (contract start date)
|
||||
|
|
@ -468,7 +468,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
valid_from_dt = datetime.fromisoformat(valid_from)
|
||||
if valid_from_dt > datetime.now(valid_from_dt.tzinfo):
|
||||
return "future"
|
||||
except (ValueError, AttributeError):
|
||||
except ValueError, AttributeError:
|
||||
pass # If parsing fails, assume active
|
||||
|
||||
return "active"
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ CONF_RELAXATION_ATTEMPTS_BEST = "relaxation_attempts_best"
|
|||
CONF_ENABLE_MIN_PERIODS_PEAK = "enable_min_periods_peak"
|
||||
CONF_MIN_PERIODS_PEAK = "min_periods_peak"
|
||||
CONF_RELAXATION_ATTEMPTS_PEAK = "relaxation_attempts_peak"
|
||||
CONF_CHART_DATA_CONFIG = "chart_data_config" # YAML config for chart data export
|
||||
|
||||
ATTRIBUTION = "Data provided by Tibber"
|
||||
|
||||
|
|
@ -487,40 +486,6 @@ PRICE_TREND_STABLE = "stable"
|
|||
PRICE_TREND_RISING = "rising"
|
||||
PRICE_TREND_STRONGLY_RISING = "strongly_rising"
|
||||
|
||||
# Sensor options (lowercase versions for ENUM device class)
|
||||
# NOTE: These constants define the valid enum options, but they are not used directly
|
||||
# in sensor/definitions.py due to import timing issues. Instead, the options are defined inline
|
||||
# in the SensorEntityDescription objects. Keep these in sync with sensor/definitions.py!
|
||||
PRICE_LEVEL_OPTIONS = [
|
||||
PRICE_LEVEL_VERY_CHEAP.lower(),
|
||||
PRICE_LEVEL_CHEAP.lower(),
|
||||
PRICE_LEVEL_NORMAL.lower(),
|
||||
PRICE_LEVEL_EXPENSIVE.lower(),
|
||||
PRICE_LEVEL_VERY_EXPENSIVE.lower(),
|
||||
]
|
||||
|
||||
PRICE_RATING_OPTIONS = [
|
||||
PRICE_RATING_LOW.lower(),
|
||||
PRICE_RATING_NORMAL.lower(),
|
||||
PRICE_RATING_HIGH.lower(),
|
||||
]
|
||||
|
||||
VOLATILITY_OPTIONS = [
|
||||
VOLATILITY_LOW.lower(),
|
||||
VOLATILITY_MODERATE.lower(),
|
||||
VOLATILITY_HIGH.lower(),
|
||||
VOLATILITY_VERY_HIGH.lower(),
|
||||
]
|
||||
|
||||
# Trend options for enum sensors (lowercase versions for ENUM device class)
|
||||
PRICE_TREND_OPTIONS = [
|
||||
PRICE_TREND_STRONGLY_FALLING,
|
||||
PRICE_TREND_FALLING,
|
||||
PRICE_TREND_STABLE,
|
||||
PRICE_TREND_RISING,
|
||||
PRICE_TREND_STRONGLY_RISING,
|
||||
]
|
||||
|
||||
# Valid options for best price maximum level filter
|
||||
# Sorted from cheap to expensive: user selects "up to how expensive"
|
||||
BEST_PRICE_MAX_LEVEL_OPTIONS = [
|
||||
|
|
@ -1019,26 +984,6 @@ def get_price_level_translation(
|
|||
return get_translation(["sensor", "current_interval_price_level", "price_levels", level], language)
|
||||
|
||||
|
||||
async def async_get_home_type_translation(
|
||||
hass: HomeAssistant,
|
||||
home_type: str,
|
||||
language: str = "en",
|
||||
) -> str | None:
|
||||
"""
|
||||
Get a localized translation for a home type asynchronously.
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant instance
|
||||
home_type: The home type (e.g., APARTMENT, HOUSE, etc.)
|
||||
language: The language code (defaults to English)
|
||||
|
||||
Returns:
|
||||
The localized home type if found, None otherwise
|
||||
|
||||
"""
|
||||
return await async_get_translation(hass, ["home_types", home_type], language)
|
||||
|
||||
|
||||
def get_home_type_translation(
|
||||
home_type: str,
|
||||
language: str = "en",
|
||||
|
|
|
|||
|
|
@ -44,9 +44,6 @@ from .time_service import TibberPricesTimeService
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Lifecycle state transition thresholds
|
||||
FRESH_TO_CACHED_SECONDS = 300 # 5 minutes
|
||||
|
||||
|
||||
def get_connection_state(coordinator: TibberPricesDataUpdateCoordinator) -> bool | None:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ class TibberPricesPeriodCalculator:
|
|||
# Internal calculations always use positive values with reverse_sort flag
|
||||
try:
|
||||
flex = abs(float(flex)) / 100 # Always positive internally
|
||||
except (TypeError, ValueError):
|
||||
except TypeError, ValueError:
|
||||
flex = (
|
||||
abs(_const.DEFAULT_BEST_PRICE_FLEX) / 100
|
||||
if not reverse_sort
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ class TibberPricesEntity(CoordinatorEntity[TibberPricesDataUpdateCoordinator]):
|
|||
home_name = f"{home_name}, {city}"
|
||||
else:
|
||||
home_name = "Tibber Home"
|
||||
except (KeyError, IndexError, TypeError):
|
||||
except KeyError, IndexError, TypeError:
|
||||
return "Tibber Home", None
|
||||
else:
|
||||
return home_name, home_type
|
||||
|
|
|
|||
|
|
@ -21,15 +21,11 @@ from __future__ import annotations
|
|||
from .attributes import (
|
||||
add_description_attributes,
|
||||
async_add_description_attributes,
|
||||
build_period_attributes,
|
||||
build_timestamp_attribute,
|
||||
)
|
||||
from .colors import add_icon_color_attribute, get_icon_color
|
||||
from .helpers import (
|
||||
find_rolling_hour_center_index,
|
||||
get_price_value,
|
||||
translate_level,
|
||||
translate_rating_level,
|
||||
)
|
||||
from .icons import (
|
||||
get_binary_sensor_icon,
|
||||
|
|
@ -46,8 +42,6 @@ __all__ = [
|
|||
"add_description_attributes",
|
||||
"add_icon_color_attribute",
|
||||
"async_add_description_attributes",
|
||||
"build_period_attributes",
|
||||
"build_timestamp_attribute",
|
||||
"find_rolling_hour_center_index",
|
||||
"get_binary_sensor_icon",
|
||||
"get_dynamic_icon",
|
||||
|
|
@ -59,6 +53,4 @@ __all__ = [
|
|||
"get_rating_sensor_icon",
|
||||
"get_trend_icon",
|
||||
"get_volatility_sensor_icon",
|
||||
"translate_level",
|
||||
"translate_rating_level",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,45 +10,6 @@ if TYPE_CHECKING:
|
|||
from ..data import TibberPricesConfigEntry # noqa: TID252
|
||||
|
||||
|
||||
def build_timestamp_attribute(interval_data: dict | None) -> str | None:
|
||||
"""
|
||||
Build timestamp attribute from interval data.
|
||||
|
||||
Extracts startsAt field consistently across all sensors.
|
||||
|
||||
Args:
|
||||
interval_data: Interval data dictionary containing startsAt field
|
||||
|
||||
Returns:
|
||||
ISO format timestamp string or None
|
||||
|
||||
"""
|
||||
if not interval_data:
|
||||
return None
|
||||
return interval_data.get("startsAt")
|
||||
|
||||
|
||||
def build_period_attributes(period_data: dict) -> dict:
|
||||
"""
|
||||
Build common period attributes (start, end, duration, timestamp).
|
||||
|
||||
Used by binary sensors for period-based entities.
|
||||
|
||||
Args:
|
||||
period_data: Period data dictionary
|
||||
|
||||
Returns:
|
||||
Dictionary with common period attributes
|
||||
|
||||
"""
|
||||
return {
|
||||
"start": period_data.get("start"),
|
||||
"end": period_data.get("end"),
|
||||
"duration_minutes": period_data.get("duration_minutes"),
|
||||
"timestamp": period_data.get("start"), # Timestamp = period start
|
||||
}
|
||||
|
||||
|
||||
def add_description_attributes( # noqa: PLR0913, PLR0912
|
||||
attributes: dict,
|
||||
platform: str,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Common helper functions for entities across platforms.
|
|||
|
||||
This module provides utility functions used by both sensor and binary_sensor platforms:
|
||||
- Price value conversion (major/subunit currency units)
|
||||
- Translation helpers (price levels, ratings)
|
||||
|
||||
- Time-based calculations (rolling hour center index)
|
||||
|
||||
These functions operate on entity-level concepts (states, translations) but are
|
||||
|
|
@ -14,7 +14,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from custom_components.tibber_prices.const import get_display_unit_factor, get_price_level_translation
|
||||
from custom_components.tibber_prices.const import get_display_unit_factor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
|
@ -22,7 +22,6 @@ if TYPE_CHECKING:
|
|||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||
from custom_components.tibber_prices.data import TibberPricesConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
def get_price_value(
|
||||
|
|
@ -62,54 +61,6 @@ def get_price_value(
|
|||
return round(price * 100, 2)
|
||||
|
||||
|
||||
def translate_level(hass: HomeAssistant, level: str) -> str:
|
||||
"""
|
||||
Translate price level to the user's language.
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant instance for language configuration
|
||||
level: Price level to translate (e.g., VERY_CHEAP, NORMAL, etc.)
|
||||
|
||||
Returns:
|
||||
Translated level string, or original level if translation not found
|
||||
|
||||
"""
|
||||
if not hass:
|
||||
return level
|
||||
|
||||
language = hass.config.language or "en"
|
||||
translated = get_price_level_translation(level, language)
|
||||
if translated:
|
||||
return translated
|
||||
|
||||
if language != "en":
|
||||
fallback = get_price_level_translation(level, "en")
|
||||
if fallback:
|
||||
return fallback
|
||||
|
||||
return level
|
||||
|
||||
|
||||
def translate_rating_level(rating: str) -> str:
|
||||
"""
|
||||
Translate price rating level to the user's language.
|
||||
|
||||
Args:
|
||||
rating: Price rating to translate (e.g., LOW, NORMAL, HIGH)
|
||||
|
||||
Returns:
|
||||
Translated rating string, or original rating if translation not found
|
||||
|
||||
Note:
|
||||
Currently returns the rating as-is. Translation mapping for ratings
|
||||
can be added here when needed, similar to translate_level().
|
||||
|
||||
"""
|
||||
# For now, ratings are returned as-is
|
||||
# Add translation mapping here when needed
|
||||
return rating
|
||||
|
||||
|
||||
def find_rolling_hour_center_index(
|
||||
all_prices: list[dict],
|
||||
current_time: datetime,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ def add_next_avg_attributes( # noqa: PLR0913
|
|||
# Extract hours from sensor key (e.g., "next_avg_3h" -> 3)
|
||||
try:
|
||||
hours = int(key.rsplit("_", maxsplit=1)[-1].replace("h", ""))
|
||||
except (ValueError, AttributeError):
|
||||
except ValueError, AttributeError:
|
||||
return
|
||||
|
||||
# Use TimeService to get the N-hour window starting from next interval
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ def _hours_to_minutes(state_value: Any) -> int | None:
|
|||
|
||||
try:
|
||||
return round(float(state_value) * 60)
|
||||
except (TypeError, ValueError):
|
||||
except TypeError, ValueError:
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ The calculator provides smart defaults:
|
|||
- No more periods → 0 for numeric values, None for timestamps
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base import TibberPricesBaseCalculator # Constants
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
PROGRESS_GRACE_PERIOD_SECONDS = 60 # Show 100% for 1 minute after period ends
|
||||
|
||||
|
||||
|
|
@ -180,9 +183,7 @@ class TibberPricesTimingCalculator(TibberPricesBaseCalculator):
|
|||
return future_periods[1]
|
||||
|
||||
# Default: return first future period
|
||||
return future_periods[0] if future_periods else None
|
||||
|
||||
return None
|
||||
return future_periods[0]
|
||||
|
||||
def _calc_remaining_minutes(self, period: dict) -> int:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ Caching strategy:
|
|||
- Current trend + next change: Cached centrally for 60s to avoid duplicate calculations
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from custom_components.tibber_prices.const import get_display_unit_factor
|
||||
|
|
@ -27,6 +26,8 @@ from custom_components.tibber_prices.utils.price import (
|
|||
from .base import TibberPricesBaseCalculator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from custom_components.tibber_prices.coordinator import (
|
||||
TibberPricesDataUpdateCoordinator,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -889,7 +889,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
if self.entity_description.entity_category == EntityCategory.DIAGNOSTIC:
|
||||
try:
|
||||
value = self.native_value
|
||||
except (KeyError, ValueError, TypeError):
|
||||
except KeyError, ValueError, TypeError:
|
||||
# If we can't get the value, hide the sensor
|
||||
return False
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ INTERVAL_PRICE_SENSORS = (
|
|||
|
||||
# NOTE: Enum options are defined inline (not imported from const.py) to avoid
|
||||
# import timing issues with Home Assistant's entity platform initialization.
|
||||
# Keep in sync with PRICE_LEVEL_OPTIONS in const.py!
|
||||
# Keep in sync with PRICE_LEVEL_* constants in const.py!
|
||||
INTERVAL_LEVEL_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
key="current_interval_price_level",
|
||||
|
|
@ -126,7 +126,7 @@ INTERVAL_LEVEL_SENSORS = (
|
|||
|
||||
# NOTE: Enum options are defined inline (not imported from const.py) to avoid
|
||||
# import timing issues with Home Assistant's entity platform initialization.
|
||||
# Keep in sync with PRICE_RATING_OPTIONS in const.py!
|
||||
# Keep in sync with PRICE_RATING_* constants in const.py!
|
||||
INTERVAL_RATING_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
key="current_interval_price_rating",
|
||||
|
|
@ -184,7 +184,7 @@ ROLLING_HOUR_PRICE_SENSORS = (
|
|||
|
||||
# NOTE: Enum options are defined inline (not imported from const.py) to avoid
|
||||
# import timing issues with Home Assistant's entity platform initialization.
|
||||
# Keep in sync with PRICE_LEVEL_OPTIONS in const.py!
|
||||
# Keep in sync with PRICE_LEVEL_* constants in const.py!
|
||||
ROLLING_HOUR_LEVEL_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
key="current_hour_price_level",
|
||||
|
|
@ -206,7 +206,7 @@ ROLLING_HOUR_LEVEL_SENSORS = (
|
|||
|
||||
# NOTE: Enum options are defined inline (not imported from const.py) to avoid
|
||||
# import timing issues with Home Assistant's entity platform initialization.
|
||||
# Keep in sync with PRICE_RATING_OPTIONS in const.py!
|
||||
# Keep in sync with PRICE_RATING_* constants in const.py!
|
||||
ROLLING_HOUR_RATING_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
key="current_hour_price_rating",
|
||||
|
|
@ -288,7 +288,7 @@ DAILY_STAT_SENSORS = (
|
|||
|
||||
# NOTE: Enum options are defined inline (not imported from const.py) to avoid
|
||||
# import timing issues with Home Assistant's entity platform initialization.
|
||||
# Keep in sync with PRICE_LEVEL_OPTIONS in const.py!
|
||||
# Keep in sync with PRICE_LEVEL_* constants in const.py!
|
||||
DAILY_LEVEL_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
key="yesterday_price_level",
|
||||
|
|
@ -319,7 +319,7 @@ DAILY_LEVEL_SENSORS = (
|
|||
|
||||
# NOTE: Enum options are defined inline (not imported from const.py) to avoid
|
||||
# import timing issues with Home Assistant's entity platform initialization.
|
||||
# Keep in sync with PRICE_RATING_OPTIONS in const.py!
|
||||
# Keep in sync with PRICE_RATING_* constants in const.py!
|
||||
DAILY_RATING_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
key="yesterday_price_rating",
|
||||
|
|
@ -692,7 +692,7 @@ PRICE_TRAJECTORY_SENSORS = (
|
|||
# ----------------------------------------------------------------------------
|
||||
# NOTE: Enum options are defined inline (not imported from const.py) to avoid
|
||||
# import timing issues with Home Assistant's entity platform initialization.
|
||||
# Keep in sync with VOLATILITY_OPTIONS in const.py!
|
||||
# Keep in sync with VOLATILITY_* constants in const.py!
|
||||
|
||||
VOLATILITY_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
|
|
|
|||
|
|
@ -5,29 +5,18 @@ This module contains helper functions specific to the sensor platform:
|
|||
- aggregate_price_data: Calculate average price from window data
|
||||
- aggregate_level_data: Aggregate price levels from intervals
|
||||
- aggregate_rating_data: Aggregate price ratings from intervals
|
||||
- aggregate_window_data: Unified aggregation based on value type
|
||||
- get_hourly_price_value: Get price for specific hour with offset
|
||||
|
||||
For shared helper functions (used by both sensor and binary_sensor platforms),
|
||||
see entity_utils/helpers.py:
|
||||
- get_price_value: Price unit conversion
|
||||
- translate_level: Price level translation
|
||||
- translate_rating_level: Rating level translation
|
||||
- find_rolling_hour_center_index: Rolling hour window calculations
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from custom_components.tibber_prices.const import get_display_unit_factor
|
||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||
from custom_components.tibber_prices.entity_utils.helpers import get_price_value
|
||||
from custom_components.tibber_prices.utils.average import calculate_mean, calculate_median
|
||||
from custom_components.tibber_prices.utils.price import (
|
||||
aggregate_price_levels,
|
||||
|
|
@ -35,7 +24,7 @@ from custom_components.tibber_prices.utils.price import (
|
|||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
|
||||
def aggregate_average_data(
|
||||
|
|
@ -106,90 +95,3 @@ def aggregate_rating_data(
|
|||
|
||||
aggregated, _ = aggregate_price_rating(differences, threshold_low, threshold_high)
|
||||
return aggregated.lower() if aggregated else None
|
||||
|
||||
|
||||
def aggregate_window_data(
|
||||
window_data: list[dict],
|
||||
value_type: str,
|
||||
threshold_low: float,
|
||||
threshold_high: float,
|
||||
config_entry: ConfigEntry,
|
||||
) -> str | float | None:
|
||||
"""
|
||||
Aggregate data from multiple intervals based on value type.
|
||||
|
||||
Unified helper that routes to appropriate aggregation function.
|
||||
|
||||
NOTE: This function is legacy code - rolling_hour calculator has its own implementation.
|
||||
|
||||
Args:
|
||||
window_data: List of price interval dictionaries.
|
||||
value_type: Type of value to aggregate ('price', 'level', or 'rating').
|
||||
threshold_low: Low threshold for rating calculation.
|
||||
threshold_high: High threshold for rating calculation.
|
||||
config_entry: Config entry to get display unit configuration.
|
||||
|
||||
Returns:
|
||||
Aggregated value (price as float, level/rating as str), or None if no data.
|
||||
|
||||
"""
|
||||
# Map value types to aggregation functions
|
||||
aggregators: dict[str, Callable] = {
|
||||
"price": lambda data: aggregate_average_data(data, config_entry)[0], # Use only average from tuple
|
||||
"level": aggregate_level_data,
|
||||
"rating": lambda data: aggregate_rating_data(data, threshold_low, threshold_high),
|
||||
}
|
||||
|
||||
aggregator = aggregators.get(value_type)
|
||||
if aggregator:
|
||||
return aggregator(window_data)
|
||||
return None
|
||||
|
||||
|
||||
def get_hourly_price_value(
|
||||
coordinator_data: dict,
|
||||
*,
|
||||
hour_offset: int,
|
||||
in_euro: bool,
|
||||
time: TibberPricesTimeService,
|
||||
) -> float | None:
|
||||
"""
|
||||
Get price for current hour or with offset.
|
||||
|
||||
Legacy helper for hourly price access (not used by Calculator Pattern).
|
||||
Kept for potential backward compatibility.
|
||||
|
||||
Args:
|
||||
coordinator_data: Coordinator data dict
|
||||
hour_offset: Hour offset from current time (positive=future, negative=past)
|
||||
in_euro: If True, return price in base currency (EUR), else minor (cents/øre)
|
||||
time: TibberPricesTimeService instance (required)
|
||||
|
||||
Returns:
|
||||
Price value, or None if not found
|
||||
|
||||
"""
|
||||
# Use TimeService to get the current time in the user's timezone
|
||||
now = time.now()
|
||||
|
||||
# Calculate the exact target datetime (not just the hour)
|
||||
# This properly handles day boundaries
|
||||
target_datetime = now.replace(microsecond=0) + timedelta(hours=hour_offset)
|
||||
target_hour = target_datetime.hour
|
||||
target_date = target_datetime.date()
|
||||
|
||||
# Get all intervals (yesterday, today, tomorrow) via helper
|
||||
all_intervals = get_intervals_for_day_offsets(coordinator_data, [-1, 0, 1])
|
||||
|
||||
# Search through all intervals to find the matching hour
|
||||
for price_data in all_intervals:
|
||||
# Parse the timestamp and convert to local time
|
||||
starts_at = time.get_interval_time(price_data)
|
||||
if starts_at is None:
|
||||
continue
|
||||
|
||||
# Compare using both hour and date for accuracy
|
||||
if starts_at.hour == target_hour and starts_at.date() == target_date:
|
||||
return get_price_value(float(price_data["total"]), in_euro=in_euro)
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ def _check_custom_cards_installed(hass: Any) -> dict[str, bool]:
|
|||
installed_cards["apexcharts-card"] = True
|
||||
if "config-template-card" in url:
|
||||
installed_cards["config-template-card"] = True
|
||||
except (AttributeError, TypeError):
|
||||
except AttributeError, TypeError:
|
||||
# Fallback: can't determine, assume not installed
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
"""
|
||||
Service handler for get_price service.
|
||||
|
||||
This service provides direct access to the interval pool for testing and development
|
||||
purposes. It uses intelligent caching to minimize API calls by fetching only missing
|
||||
intervals from the API.
|
||||
This service fetches raw price interval data for any time range using the
|
||||
interval pool's intelligent caching. Only intervals not already cached are
|
||||
fetched from the Tibber API.
|
||||
|
||||
Functions:
|
||||
handle_get_price: Service handler for fetching price data
|
||||
|
||||
Used for:
|
||||
- Testing interval pool caching logic
|
||||
- Development and debugging of historical data queries
|
||||
- Verifying gap detection and cache hit/miss behavior
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -34,16 +34,6 @@ class TibberPricesSwitchEntityDescription(SwitchEntityDescription):
|
|||
# BEST PRICE PERIOD CONFIGURATION OVERRIDES (Boolean)
|
||||
# ============================================================================
|
||||
|
||||
BEST_PRICE_SWITCH_ENTITIES = (
|
||||
SwitchEntityDescription(
|
||||
key="best_price_enable_relaxation_override",
|
||||
translation_key="best_price_enable_relaxation_override",
|
||||
icon="mdi:arrow-down-bold-circle",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
# Custom descriptions with extra fields
|
||||
BEST_PRICE_SWITCH_ENTITY_DESCRIPTIONS = (
|
||||
TibberPricesSwitchEntityDescription(
|
||||
|
|
|
|||
|
|
@ -135,10 +135,11 @@ const config: Config = {
|
|||
docs: {
|
||||
sidebar: {
|
||||
hideable: true,
|
||||
autoCollapseCategories: true,
|
||||
autoCollapseCategories: false,
|
||||
},
|
||||
},
|
||||
navbar: {
|
||||
hideOnScroll: true,
|
||||
title: 'Tibber Prices HA',
|
||||
logo: {
|
||||
alt: 'Tibber Prices Integration Logo',
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const sidebars: SidebarsConfig = {
|
|||
{
|
||||
type: 'category',
|
||||
label: '🏗️ Architecture',
|
||||
link: { type: 'doc', id: 'architecture' },
|
||||
items: ['architecture', 'timer-architecture', 'caching-strategy', 'api-reference'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
|
|
@ -25,6 +26,7 @@ const sidebars: SidebarsConfig = {
|
|||
{
|
||||
type: 'category',
|
||||
label: '💻 Development',
|
||||
link: { type: 'doc', id: 'setup' },
|
||||
items: ['setup', 'coding-guidelines', 'critical-patterns', 'repairs-system', 'debugging'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
|
|
@ -32,6 +34,7 @@ const sidebars: SidebarsConfig = {
|
|||
{
|
||||
type: 'category',
|
||||
label: '📐 Advanced Topics',
|
||||
link: { type: 'doc', id: 'period-calculation-theory' },
|
||||
items: ['period-calculation-theory', 'refactoring-guide', 'performance', 'recorder-optimization'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
|
|
@ -39,6 +42,7 @@ const sidebars: SidebarsConfig = {
|
|||
{
|
||||
type: 'category',
|
||||
label: '📝 Contributing',
|
||||
link: { type: 'doc', id: 'contributing' },
|
||||
items: ['contributing'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
|
|
@ -46,6 +50,7 @@ const sidebars: SidebarsConfig = {
|
|||
{
|
||||
type: 'category',
|
||||
label: '🚀 Release',
|
||||
link: { type: 'doc', id: 'release-management' },
|
||||
items: ['release-management', 'testing'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ When you write YAML directly (automations, scripts, Lovelace dashboard cards), y
|
|||
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
|
||||
5. Paste the value wherever you see `YOUR_CONFIG_ENTRY_ID` in the YAML examples
|
||||
|
||||
The ID looks like a long alphanumeric string, for example `01JKPC7AB3EF4GH5IJ6KL7MN8P`.
|
||||
|
||||
|
|
@ -30,8 +30,6 @@ If you have configured more than one Tibber home, each home has its own entry ID
|
|||
|
||||
## 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.
|
||||
|
|
@ -51,7 +49,7 @@ If you have configured more than one Tibber home, each home has its own entry ID
|
|||
```yaml
|
||||
service: tibber_prices.get_chartdata
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
day: ["today", "tomorrow"]
|
||||
output_format: array_of_objects
|
||||
response_variable: chart_data
|
||||
|
|
@ -92,7 +90,7 @@ Omit the `day` parameter to get a dynamic 48-hour rolling window that automatica
|
|||
```yaml
|
||||
service: tibber_prices.get_chartdata
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
# Omit 'day' for rolling window
|
||||
output_format: array_of_objects
|
||||
response_variable: chart_data
|
||||
|
|
@ -111,7 +109,7 @@ Get best price periods as summaries instead of intervals:
|
|||
```yaml
|
||||
service: tibber_prices.get_chartdata
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
period_filter: best_price # or peak_price
|
||||
day: ["today", "tomorrow"]
|
||||
include_level: true
|
||||
|
|
@ -124,7 +122,7 @@ response_variable: periods
|
|||
```yaml
|
||||
service: tibber_prices.get_chartdata
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_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
|
||||
|
|
@ -150,7 +148,7 @@ You can include the raw energy price (spot price) and/or tax component in chart
|
|||
```yaml
|
||||
service: tibber_prices.get_chartdata
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
day: ["today", "tomorrow"]
|
||||
include_energy: true
|
||||
include_tax: true
|
||||
|
|
@ -168,7 +166,7 @@ Returns data points like:
|
|||
}
|
||||
```
|
||||
|
||||
**Use case — Solar feed-in chart:** Overlay the energy price (what you earn by exporting) alongside the total price to visualize the best export windows. See [Sensors — Energy Price & Tax Breakdown](sensors.md#energy-price--tax-breakdown) for more use cases.
|
||||
**Use case — Solar feed-in chart:** Overlay the energy price (what you earn by exporting) alongside the total price to visualize the best export windows. See [Energy & Tax Attributes](sensors-energy-tax.md) for more use cases.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -201,7 +199,7 @@ Returns data points like:
|
|||
```yaml
|
||||
service: tibber_prices.get_apexcharts_yaml
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_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
|
||||
|
|
@ -230,7 +228,7 @@ Rolling window configurations automatically integrate with the `chart_metadata`
|
|||
```yaml
|
||||
service: tibber_prices.get_apexcharts_yaml
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
day: today
|
||||
level_type: rating_level
|
||||
response_variable: config
|
||||
|
|
@ -245,7 +243,7 @@ type: custom:apexcharts-card
|
|||
```yaml
|
||||
service: tibber_prices.get_apexcharts_yaml
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
# Omit 'day' for rolling window (or use 'rolling_window')
|
||||
level_type: level # 5-level classification
|
||||
highlight_best_price: true
|
||||
|
|
@ -298,13 +296,64 @@ Use the response in Lovelace dashboards by copying the generated YAML.
|
|||
```yaml
|
||||
service: tibber_prices.refresh_user_data
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_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).
|
||||
|
||||
---
|
||||
|
||||
### tibber_prices.get_price
|
||||
|
||||
**Purpose:** Fetches raw price interval data for any time range. Uses intelligent caching — only intervals not already cached are fetched from the Tibber API.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Description | Required |
|
||||
|-----------|-------------|----------|
|
||||
| `entry_id` | Integration entry ID | Yes |
|
||||
| `start_time` | Start of the time range | Yes |
|
||||
| `end_time` | End of the time range | Yes |
|
||||
|
||||
**Example:**
|
||||
|
||||
```yaml
|
||||
service: tibber_prices.get_price
|
||||
data:
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
start_time: "2025-11-01T00:00:00"
|
||||
end_time: "2025-11-02T00:00:00"
|
||||
response_variable: price_data
|
||||
```
|
||||
|
||||
**Response Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"home_id": "abc-123",
|
||||
"start_time": "2025-11-01T00:00:00+01:00",
|
||||
"end_time": "2025-11-02T00:00:00+01:00",
|
||||
"interval_count": 96,
|
||||
"price_info": [
|
||||
{
|
||||
"startsAt": "2025-11-01T00:00:00+01:00",
|
||||
"total": 0.2534,
|
||||
"energy": 0.1218,
|
||||
"tax": 0.1316
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Fetching historical price data for analysis
|
||||
- Comparing prices across arbitrary date ranges
|
||||
- Building custom charts with historical data
|
||||
|
||||
**Note:** Times are automatically converted to your Tibber home's timezone. The interval pool caches previously fetched intervals, so repeated calls for the same range are fast.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
- [Price-Based Automations](#price-based-automations)
|
||||
- [Volatility-Aware Automations](#volatility-aware-automations)
|
||||
- [Best Hour Detection](#best-hour-detection)
|
||||
- [ApexCharts Cards](#apexcharts-cards)
|
||||
- [Charts & Visualizations](#charts--visualizations)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
>
|
||||
> 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**).
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
## Price-Based Automations
|
||||
|
||||
|
|
@ -269,7 +269,7 @@ automation:
|
|||
</details>
|
||||
|
||||
:::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.
|
||||
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-trends.md#how-to-use-trend-sensors-for-decisions) in the sensor documentation for details.
|
||||
:::
|
||||
|
||||
### Sensor Combination Quick Reference
|
||||
|
|
@ -277,10 +277,10 @@ A common misconception: **"rising" does NOT mean "too late"**. It means your cur
|
|||
| 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) |
|
||||
| **"Will prices go up or down?"** | <EntityRef id="current_price_trend" noStrong>`current_price_trend`</EntityRef> state |
|
||||
| **"When will the trend change?"** | <EntityRef id="next_price_trend_change" noStrong>`next_price_trend_change`</EntityRef> state |
|
||||
| **"How cheap will it get?"** | `next_Nh_avg` attribute on trend sensors |
|
||||
| **"Is the price drop meaningful?"** | `today_s_price_volatility` (not low = meaningful) |
|
||||
| **"Is the price drop meaningful?"** | <EntityRef id="today_volatility" noStrong>`today_s_price_volatility`</EntityRef> |
|
||||
| **"Ride the full cheap wave"** | `rating_level` + `current_price_trend` + `best_price_period` |
|
||||
|
||||
---
|
||||
|
|
@ -502,97 +502,6 @@ automation:
|
|||
|
||||
---
|
||||
|
||||
## ApexCharts Cards
|
||||
## Charts & Visualizations
|
||||
|
||||
> ⚠️ **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
|
||||
> **Looking for chart configurations?** See the **[Chart Examples Guide](chart-examples.md)** for ApexCharts card configurations, rolling window modes, and more.
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ This guide showcases the different chart configurations available through the `t
|
|||
|
||||
> **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**).
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
:::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.
|
||||
Every example below contains `entry_id: YOUR_CONFIG_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`.
|
||||
**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_CONFIG_ENTRY_ID`.
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
|
@ -42,7 +42,7 @@ The integration can generate 4 different chart modes, each optimized for specifi
|
|||
```yaml
|
||||
service: tibber_prices.get_apexcharts_yaml
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
day: today
|
||||
level_type: rating_level
|
||||
highlight_best_price: true
|
||||
|
|
@ -72,7 +72,7 @@ data:
|
|||
```yaml
|
||||
service: tibber_prices.get_apexcharts_yaml
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
# Omit 'day' for rolling window
|
||||
level_type: rating_level
|
||||
highlight_best_price: true
|
||||
|
|
@ -106,7 +106,7 @@ data:
|
|||
```yaml
|
||||
service: tibber_prices.get_apexcharts_yaml
|
||||
data:
|
||||
entry_id: YOUR_ENTRY_ID
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
day: rolling_window_autozoom
|
||||
level_type: rating_level
|
||||
highlight_best_price: true
|
||||
|
|
@ -291,7 +291,7 @@ cards:
|
|||
## 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
|
||||
- **[Chart Metadata Sensor](sensors-overview.md#chart-metadata)**: Learn about dynamic Y-axis scaling
|
||||
- **[Period Calculation Guide](period-calculation.md)**: Configure best price period detection
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ 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 (1–2 hours, covering only the absolute minimum), but prices can remain favorable for 4–6 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.
|
||||
**Why this matters:** On these days, the Best Price Period may be short (1–2 hours, covering only the absolute minimum), but prices can remain favorable for 4–6 hours. By combining [trend sensors](sensors-trends.md) with [price levels](sensors-ratings-levels.md) 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.
|
||||
|
||||
|
|
@ -110,5 +110,5 @@ 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
|
||||
- [Sensors Overview](sensors-overview.md) - How to use sensor data
|
||||
- [Automation Examples](automation-examples.md) - Practical use cases
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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**).
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ Configure the Coefficient of Variation (CV) boundaries:
|
|||
Configure detection of favorable price windows. Three collapsible sections:
|
||||
|
||||
**Period Settings:**
|
||||
- Minimum period length (default: 30 min)
|
||||
- Minimum period length (default: 60 min)
|
||||
- Maximum price level to include (default: CHEAP)
|
||||
- Gap tolerance: how many expensive intervals to bridge (default: 1)
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ Configure when trend sensors report rising/falling:
|
|||
| **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.
|
||||
Thresholds are [volatility-adaptive](sensors-trends.md): automatically widened on volatile days to prevent constant state changes.
|
||||
|
||||
### Step 9: Chart Data Export (Legacy)
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ Information page for the legacy chart data export sensor. For new setups, use th
|
|||
|
||||
### Average Sensor Display Settings
|
||||
|
||||
**Location:** Settings → Devices & Services → Tibber Prices → Configure → Step 6
|
||||
**Location:** Settings → Devices & Services → Tibber Prices → Configure → **General Settings**
|
||||
|
||||
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).
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ This setting applies to:
|
|||
- 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.
|
||||
See the **[Average Sensors](sensors-average.md)** for detailed examples.
|
||||
|
||||
#### Choosing Your Display
|
||||
|
||||
|
|
@ -214,25 +214,25 @@ When enabled, these entities override the corresponding Options Flow 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 |
|
||||
| <EntityRef id="best_price_flex_override">Best Price: Flexibility</EntityRef> | Number | 0-50% | Maximum above daily minimum for "best price" intervals |
|
||||
| <EntityRef id="best_price_min_distance_override">Best Price: Minimum Distance</EntityRef> | Number | -50-0% | Required distance below daily average |
|
||||
| <EntityRef id="best_price_min_period_length_override">Best Price: Minimum Period Length</EntityRef> | Number | 15-180 min | Shortest period duration to consider |
|
||||
| <EntityRef id="best_price_min_periods_override">Best Price: Minimum Periods</EntityRef> | Number | 1-10 | Target number of periods per day |
|
||||
| <EntityRef id="best_price_relaxation_attempts_override">Best Price: Relaxation Attempts</EntityRef> | Number | 1-12 | Steps to try when relaxing criteria |
|
||||
| <EntityRef id="best_price_gap_tolerance_override">Best Price: Gap Tolerance</EntityRef> | Number | 0-8 | Consecutive intervals allowed above threshold |
|
||||
| <EntityRef id="best_price_achieve_min_count_override">Best Price: Achieve Minimum Count</EntityRef> | 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 |
|
||||
| <EntityRef id="peak_price_flex_override">Peak Price: Flexibility</EntityRef> | Number | -50-0% | Maximum below daily maximum for "peak price" intervals |
|
||||
| <EntityRef id="peak_price_min_distance_override">Peak Price: Minimum Distance</EntityRef> | Number | 0-50% | Required distance above daily average |
|
||||
| <EntityRef id="peak_price_min_period_length_override">Peak Price: Minimum Period Length</EntityRef> | Number | 15-180 min | Shortest period duration to consider |
|
||||
| <EntityRef id="peak_price_min_periods_override">Peak Price: Minimum Periods</EntityRef> | Number | 1-10 | Target number of periods per day |
|
||||
| <EntityRef id="peak_price_relaxation_attempts_override">Peak Price: Relaxation Attempts</EntityRef> | Number | 1-12 | Steps to try when relaxing criteria |
|
||||
| <EntityRef id="peak_price_gap_tolerance_override">Peak Price: Gap Tolerance</EntityRef> | Number | 0-8 | Consecutive intervals allowed below threshold |
|
||||
| <EntityRef id="peak_price_achieve_min_count_override">Peak Price: Achieve Minimum Count</EntityRef> | Switch | On/Off | Enable relaxation algorithm |
|
||||
|
||||
### How Runtime Overrides Work
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
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**).
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
## Basic Price Display Card
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
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**).
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
## What are Dynamic Icons?
|
||||
|
||||
|
|
@ -26,9 +26,9 @@ To see which icon a sensor currently uses:
|
|||
|
||||
**Common sensor types with dynamic icons:**
|
||||
|
||||
- Price level sensors (e.g., `current_price_level`)
|
||||
- Price rating sensors (e.g., `current_price_rating`)
|
||||
- Volatility sensors (e.g., `today_s_price_volatility`)
|
||||
- Price level sensors (e.g., `current_price_level` → `current_interval_price_level`)
|
||||
- Price rating sensors (e.g., `current_price_rating` → `current_interval_price_rating`)
|
||||
- Volatility sensors (e.g., `today_s_price_volatility` → `today_volatility`)
|
||||
- Binary sensors (e.g., `best_price_period`, `peak_price_period`)
|
||||
|
||||
## Using Dynamic Icons in Your Dashboard
|
||||
|
|
@ -176,5 +176,5 @@ The exact icons are chosen to be intuitive and meaningful in the Home Assistant
|
|||
## See Also
|
||||
|
||||
- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state
|
||||
- [Sensors Reference](sensors.md) - Complete list of available sensors
|
||||
- [Sensors Overview](sensors-overview.md) - Complete list of available sensors
|
||||
- [Automation Examples](automation-examples.md) - Use dynamic icons in automations
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ If you see unexpected units, check your configuration in the integration options
|
|||
|
||||
## 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**).
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
### How do I run dishwasher during cheap period?
|
||||
|
||||
|
|
|
|||
|
|
@ -115,5 +115,5 @@ Quick reference for terms used throughout the documentation.
|
|||
|
||||
💡 **See Also:**
|
||||
- [Core Concepts](concepts.md) - In-depth explanations
|
||||
- [Sensors](sensors.md) - How sensors use these concepts
|
||||
- [Sensors Overview](sensors-overview.md) - How sensors use these concepts
|
||||
- [Period Calculation](period-calculation.md) - Deep dive into period detection
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Many sensors in the Tibber Prices integration provide an `icon_color` attribute
|
|||
|
||||
> **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**).
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
## What is icon_color?
|
||||
|
||||
|
|
@ -40,12 +40,12 @@ Many sensors provide the `icon_color` attribute for dynamic styling. To see if a
|
|||
|
||||
**Common sensor types with icon_color:**
|
||||
|
||||
- Price level sensors (e.g., `current_price_level`)
|
||||
- Price rating sensors (e.g., `current_price_rating`)
|
||||
- Volatility sensors (e.g., `today_s_price_volatility`)
|
||||
- Price level sensors (e.g., `current_price_level` → `current_interval_price_level`)
|
||||
- Price rating sensors (e.g., `current_price_rating` → `current_interval_price_rating`)
|
||||
- Volatility sensors (e.g., `today_s_price_volatility` → `today_volatility`)
|
||||
- Price outlook sensors (e.g., `price_outlook_3h`)
|
||||
- Binary sensors (e.g., `best_price_period`, `peak_price_period`)
|
||||
- Timing sensors (e.g., `best_price_time_until_start`, `best_price_progress`)
|
||||
- Timing sensors (e.g., `best_price_time_until_start` → `best_price_next_in_minutes`, `best_price_progress`)
|
||||
|
||||
The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray.
|
||||
|
||||
|
|
@ -484,6 +484,6 @@ styles:
|
|||
|
||||
## See Also
|
||||
|
||||
- [Sensors Reference](sensors.md) - Complete list of available sensors
|
||||
- [Sensors Overview](sensors-overview.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
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ This is an independent, community-maintained custom integration. It is **not** a
|
|||
- **[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
|
||||
- **[Sensors](sensors-overview.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
|
||||
|
|
|
|||
|
|
@ -2,22 +2,16 @@
|
|||
|
||||
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**).
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Configuration Guide](#configuration-guide)
|
||||
- [Understanding Relaxation](#understanding-relaxation)
|
||||
- [Understanding Relaxation](#understanding-relaxation) → [Full Guide](period-relaxation.md)
|
||||
- [Common Scenarios](#common-scenarios)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Fewer Periods Than Configured](#fewer-periods-than-configured)
|
||||
- [No Periods Found](#no-periods-found)
|
||||
- [Periods Split Into Small Pieces](#periods-split-into-small-pieces)
|
||||
- [Understanding Sensor Attributes](#understanding-sensor-attributes)
|
||||
- [Midnight Price Classification Changes](#midnight-price-classification-changes)
|
||||
- [Advanced Topics](#advanced-topics)
|
||||
- [Advanced Topics](#advanced-topics)
|
||||
|
||||
---
|
||||
|
|
@ -28,8 +22,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for
|
|||
|
||||
The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price):
|
||||
|
||||
- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water
|
||||
- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads
|
||||
- <EntityRef id="best_price_period">Best Price Periods</EntityRef> 🟢 - When to run your dishwasher, charge your EV, or heat water
|
||||
- <EntityRef id="peak_price_period">Peak Price Periods</EntityRef> 🔴 - When to reduce consumption or defer non-essential loads
|
||||
|
||||
### Default Behavior
|
||||
|
||||
|
|
@ -78,6 +72,14 @@ 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.
|
||||
|
||||
:::info What "Best Price" means
|
||||
A Best Price Period is the **cheapest contiguous block** of time — a stretch of consecutive intervals where prices stay close to the daily minimum. It is **not** a fixed-length sliding window that shifts to center around the minimum.
|
||||
|
||||
On a V-shaped day (prices drop sharply to a minimum, then rise again), the period therefore starts **at** the low point, not before it — because the intervals leading up to the minimum did not yet meet the price criteria. Only the intervals near the bottom of the V qualify as a coherent cheap block.
|
||||
|
||||
**For flexible loads** (e.g., heat pump, battery charging): you can easily ride the full cheap wave by combining the period sensor with the price level and trend sensors. See [V-Shaped Price Days in Automation Examples](./automation-examples.md#understanding-v-shaped-price-days).
|
||||
:::
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["96 intervals per day"] --> B{"① Flexibility<br/><small>Close to MIN/MAX?</small>"}
|
||||
|
|
@ -357,109 +359,23 @@ best_price_max_level: cheap # Only show objectively CHEAP periods
|
|||
|
||||
## Understanding Relaxation
|
||||
|
||||
### What Is Relaxation?
|
||||
Sometimes strict filters find too few periods. **Relaxation automatically loosens filters** until a minimum number of periods is found — enabled by default.
|
||||
|
||||
Sometimes, strict filters find too few periods (or none). **Relaxation automatically loosens filters** until a minimum number of periods is found.
|
||||
**Key benefits:**
|
||||
- Each day gets exactly the flexibility it needs (per-day independence)
|
||||
- Uses a matrix approach: N flex levels × 2 filter combinations
|
||||
- Stops as soon as enough periods are 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).
|
||||
**→ [Full Relaxation Guide](period-relaxation.md)** — How it works, the adaptive matrix, choosing attempts, and diagnostics.
|
||||
|
||||
---
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
:::tip V-shaped price days
|
||||
On days with a sharp price dip (e.g. solar midday surplus), the Best Price Period covers only the absolute minimum. The surrounding cheap hours are intentional — the integration shows you the cheapest contiguous block, not a fixed-length window centered on the minimum. To run a device during the full cheap window, see [Understanding V-Shaped Price Days](./automation-examples.md#understanding-v-shaped-price-days) in the Automation Examples.
|
||||
:::
|
||||
|
||||
### Scenario 1: Simple Best Price (Default)
|
||||
|
||||
**Goal:** Find the cheapest time each day to run dishwasher
|
||||
|
|
@ -838,8 +754,3 @@ The Tibber API provides price levels for each 15-minute interval:
|
|||
- `NORMAL` - Around average
|
||||
- `EXPENSIVE` - Above average
|
||||
- `VERY_EXPENSIVE` - Significantly above average
|
||||
|
||||
---
|
||||
|
||||
**Last updated:** November 20, 2025
|
||||
**Integration version:** 2.0+
|
||||
|
|
|
|||
129
docs/user/docs/period-relaxation.md
Normal file
129
docs/user/docs/period-relaxation.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Understanding Relaxation
|
||||
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
Relaxation is the automatic filter-loosening mechanism that ensures your [Best/Peak Price periods](period-calculation.md) always find results — even on days with unusual price patterns.
|
||||
|
||||
---
|
||||
|
||||
## 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).
|
||||
|
||||
## Diagnosing Relaxation Behavior
|
||||
|
||||
Check the period sensor attributes to understand what happened:
|
||||
|
||||
```yaml
|
||||
# Entity: binary_sensor.<home_name>_best_price_period
|
||||
|
||||
relaxation_active: true # This day needed relaxation
|
||||
relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed
|
||||
min_periods_configured: 2 # Your target
|
||||
periods_found_total: 3 # What was actually found
|
||||
```
|
||||
|
||||
| Attribute | Meaning |
|
||||
|-----------|---------|
|
||||
| `relaxation_active: false` | Original filters were sufficient |
|
||||
| `relaxation_active: true` | Filters were loosened to find enough periods |
|
||||
| `relaxation_level` | Shows exactly which flex% and filter combo succeeded |
|
||||
| `relaxation_incomplete: true` | All attempts exhausted, still short of target |
|
||||
| `flat_days_detected: 1` | Uniform prices → target reduced to 1 (expected) |
|
||||
|
||||
**See also:** [Period Calculation — Troubleshooting](period-calculation.md#troubleshooting) for more diagnostic guidance.
|
||||
261
docs/user/docs/sensor-reference.md
Normal file
261
docs/user/docs/sensor-reference.md
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
---
|
||||
comments: false
|
||||
---
|
||||
|
||||
# Entity Reference (All Languages)
|
||||
|
||||
<EntitySearch />
|
||||
|
||||
## How to Find Your Entity in Home Assistant
|
||||
|
||||
**Entity ID pattern:** `sensor.<home_name>_<suffix>`
|
||||
|
||||
- `<home_name>` is generated from your Tibber home display name (lowercase, spaces replaced with underscores)
|
||||
- `<suffix>` is shown in the **Entity ID suffix** column below
|
||||
|
||||
**Three ways to find an entity:**
|
||||
|
||||
1. **Search above** — Type the entity name in your language to filter the tables below
|
||||
2. **Device page** — Go to **Settings → Devices & Services → Tibber Prices** →
|
||||
click your home device → all entities are listed
|
||||
3. **Developer Tools** — Go to **Developer Tools → States** →
|
||||
type `tibber` in the filter
|
||||
|
||||
:::tip
|
||||
You can also use your browser's built-in search (**Ctrl+F** / **Cmd+F**) to search the full page text.
|
||||
:::
|
||||
|
||||
**Enabled by default:** The ✅ column shows whether a sensor is enabled by default.
|
||||
Sensors marked ❌ must be enabled manually via
|
||||
**Settings → Devices & Services → Entities** → find the entity → toggle **Enabled**.
|
||||
|
||||
**Detailed documentation:** See the **[Sensors Overview](sensors-overview.md)** for detailed
|
||||
explanations of each sensor's purpose, attributes, and automation examples.
|
||||
|
||||
---
|
||||
|
||||
## Sensors
|
||||
|
||||
### Core Price Sensors
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-current_interval_price" class="entity-anchor"></span>`current_interval_price` | Current Electricity Price | Aktueller Strompreis | Nåværende strømpris | Huidige Elektriciteitsprijs | Aktuellt elpris | ✅ |
|
||||
| <span id="ref-current_interval_price_base" class="entity-anchor"></span>`current_interval_price_base` | Current Electricity Price (Energy Dashboard) | Aktueller Strompreis (Energie-Dashboard) | Nåværende strømpris (Energi-dashboard) | Huidige Elektriciteitsprijs (Energie Dashboard) | Aktuellt elpris (Energidashboard) | ✅ |
|
||||
| <span id="ref-next_interval_price" class="entity-anchor"></span>`next_interval_price` | Next Electricity Price | Nächster Strompreis | Neste strømpris | Volgende Elektriciteitsprijs | Nästa elpris | ✅ |
|
||||
| <span id="ref-previous_interval_price" class="entity-anchor"></span>`previous_interval_price` | Previous Electricity Price | Vorheriger Strompreis | Forrige strømpris | Vorige Elektriciteitsprijs | Föregående elpris | ❌ |
|
||||
|
||||
### Hourly Average Sensors
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-current_hour_average_price" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`current_hour_average_price` | ⌀ Hourly Price Current | ⌀ Stunden-Preis aktuell | ⌀ Timepris nåværende | ⌀ Uurprijs Huidig | ⌀ Timpris aktuell | ✅ |
|
||||
| <span id="ref-next_hour_average_price" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`next_hour_average_price` | ⌀ Hourly Price Next | ⌀ Stunden-Preis nächste Stunde | ⌀ Timepris neste | ⌀ Uurprijs Volgend | ⌀ Timpris nästa | ✅ |
|
||||
|
||||
### Daily Statistics
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-lowest_price_today" class="entity-anchor" data-refs="sensors-overview#daily-min-max"></span>`lowest_price_today` | Today's Lowest Price | Mindestpreis heute | Dagens laveste pris | Laagste Prijs Vandaag | Dagens lägsta pris | ✅ |
|
||||
| <span id="ref-highest_price_today" class="entity-anchor" data-refs="sensors-overview#daily-min-max"></span>`highest_price_today` | Today's Highest Price | Höchstpreis heute | Dagens høyeste pris | Hoogste Prijs Vandaag | Dagens högsta pris | ✅ |
|
||||
| <span id="ref-average_price_today" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`average_price_today` | ⌀ Price Today | ⌀ Preis heute | ⌀ Pris i dag | ⌀ Prijs Vandaag | ⌀ Pris idag | ✅ |
|
||||
| <span id="ref-lowest_price_tomorrow" class="entity-anchor" data-refs="sensors-overview#daily-min-max"></span>`lowest_price_tomorrow` | Tomorrow's Lowest Price | Mindestpreis morgen | Morgendagens laveste pris | Laagste Prijs Morgen | Morgondagens lägsta pris | ✅ |
|
||||
| <span id="ref-highest_price_tomorrow" class="entity-anchor" data-refs="sensors-overview#daily-min-max"></span>`highest_price_tomorrow` | Tomorrow's Highest Price | Höchstpreis morgen | Morgendagens høyeste pris | Hoogste Prijs Morgen | Morgondagens högsta pris | ✅ |
|
||||
| <span id="ref-average_price_tomorrow" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`average_price_tomorrow` | ⌀ Price Tomorrow | ⌀ Preis morgen | ⌀ Pris i morgen | ⌀ Prijs Morgen | ⌀ Pris imorgon | ✅ |
|
||||
|
||||
### 24h Window Sensors
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-trailing_price_average" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`trailing_price_average` | ⌀ Price Trailing 24h | ⌀ Preis nachlaufend 24h | ⌀ Pris glidende 24t | ⌀ Prijs Afgelopen 24u | ⌀ Pris glidande 24h | ❌ |
|
||||
| <span id="ref-leading_price_average" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`leading_price_average` | ⌀ Price Leading 24h | ⌀ Preis vorlaufend 24h | ⌀ Pris fremtidig 24t | ⌀ Prijs Komende 24u | ⌀ Pris framåt 24h | ❌ |
|
||||
| <span id="ref-trailing_price_min" class="entity-anchor" data-refs="sensors-overview#24-hour-rolling-min-max"></span>`trailing_price_min` | Trailing 24h Minimum Price | 24h-Mindestpreis nachlaufend | Glidende 24t minimumspris | Afgelopen 24u Minimumprijs | Glidande 24h minimipris | ❌ |
|
||||
| <span id="ref-trailing_price_max" class="entity-anchor" data-refs="sensors-overview#24-hour-rolling-min-max"></span>`trailing_price_max` | Trailing 24h Maximum Price | 24h-Höchstpreis nachlaufend | Glidende 24t maksimumspris | Afgelopen 24u Maximumprijs | Glidande 24h maximipris | ❌ |
|
||||
| <span id="ref-leading_price_min" class="entity-anchor" data-refs="sensors-overview#24-hour-rolling-min-max"></span>`leading_price_min` | Leading 24h Minimum Price | 24h-Mindestpreis vorlaufend | Fremtidig 24t minimumspris | Komende 24u Minimumprijs | Framåt 24h minimipris | ❌ |
|
||||
| <span id="ref-leading_price_max" class="entity-anchor" data-refs="sensors-overview#24-hour-rolling-min-max"></span>`leading_price_max` | Leading 24h Maximum Price | 24h-Höchstpreis vorlaufend | Fremtidig 24t maksimumspris | Komende 24u Maximumprijs | Framåt 24h maximipris | ❌ |
|
||||
|
||||
### Future Price Averages
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-next_avg_1h" class="entity-anchor"></span>`next_avg_1h` | ⌀ Price Next 1h | ⌀ Preis nächste 1h | ⌀ Pris neste 1t | ⌀ Prijs Komende 1u | ⌀ Pris nästa 1h | ✅ |
|
||||
| <span id="ref-next_avg_2h" class="entity-anchor"></span>`next_avg_2h` | ⌀ Price Next 2h | ⌀ Preis nächste 2h | ⌀ Pris neste 2t | ⌀ Prijs Komende 2u | ⌀ Pris nästa 2h | ✅ |
|
||||
| <span id="ref-next_avg_3h" class="entity-anchor"></span>`next_avg_3h` | ⌀ Price Next 3h | ⌀ Preis nächste 3h | ⌀ Pris neste 3t | ⌀ Prijs Komende 3u | ⌀ Pris nästa 3h | ✅ |
|
||||
| <span id="ref-next_avg_4h" class="entity-anchor"></span>`next_avg_4h` | ⌀ Price Next 4h | ⌀ Preis nächste 4h | ⌀ Pris neste 4t | ⌀ Prijs Komende 4u | ⌀ Pris nästa 4h | ✅ |
|
||||
| <span id="ref-next_avg_5h" class="entity-anchor"></span>`next_avg_5h` | ⌀ Price Next 5h | ⌀ Preis nächste 5h | ⌀ Pris neste 5t | ⌀ Prijs Komende 5u | ⌀ Pris nästa 5h | ✅ |
|
||||
| <span id="ref-next_avg_6h" class="entity-anchor"></span>`next_avg_6h` | ⌀ Price Next 6h | ⌀ Preis nächste 6h | ⌀ Pris neste 6t | ⌀ Prijs Komende 6u | ⌀ Pris nästa 6h | ❌ |
|
||||
| <span id="ref-next_avg_8h" class="entity-anchor"></span>`next_avg_8h` | ⌀ Price Next 8h | ⌀ Preis nächste 8h | ⌀ Pris neste 8t | ⌀ Prijs Komende 8u | ⌀ Pris nästa 8h | ❌ |
|
||||
| <span id="ref-next_avg_12h" class="entity-anchor"></span>`next_avg_12h` | ⌀ Price Next 12h | ⌀ Preis nächste 12h | ⌀ Pris neste 12t | ⌀ Prijs Komende 12u | ⌀ Pris nästa 12h | ❌ |
|
||||
|
||||
### Price Level Sensors
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-current_interval_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`current_interval_price_level` | Current Price Level | Aktuelles Preisniveau | Nåværende prisnivå | Huidig Prijsniveau | Aktuell prisnivå | ✅ |
|
||||
| <span id="ref-next_interval_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`next_interval_price_level` | Next Price Level | Nächstes Preisniveau | Neste prisnivå | Volgend Prijsniveau | Nästa prisnivå | ✅ |
|
||||
| <span id="ref-previous_interval_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`previous_interval_price_level` | Previous Price Level | Vorheriges Preisniveau | Forrige prisnivå | Vorig Prijsniveau | Föregående prisnivå | ❌ |
|
||||
| <span id="ref-current_hour_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`current_hour_price_level` | Current Hour Price Level | Aktuelles Stunden-Preisniveau | Nåværende timepris nivå | Huidig Uur Prijsniveau | Aktuell timprisnivå | ✅ |
|
||||
| <span id="ref-next_hour_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`next_hour_price_level` | Next Hour Price Level | Nächstes Stunden-Preisniveau | Neste timepris nivå | Volgend Uur Prijsniveau | Nästa timprisnivå | ✅ |
|
||||
| <span id="ref-yesterday_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`yesterday_price_level` | Yesterday's Price Level | Preisniveau gestern | Prisnivå i går | Gisteren Prijsniveau | Gårdagens prisnivå | ❌ |
|
||||
| <span id="ref-today_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`today_price_level` | Today's Price Level | Preisniveau heute | Prisnivå i dag | Vandaag Prijsniveau | Dagens prisnivå | ✅ |
|
||||
| <span id="ref-tomorrow_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`tomorrow_price_level` | Tomorrow's Price Level | Preisniveau morgen | Prisnivå i morgen | Morgen Prijsniveau | Morgondagens prisnivå | ✅ |
|
||||
|
||||
### Price Rating Sensors
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-current_interval_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`current_interval_price_rating` | Current Price Rating | Aktuelle Preisbewertung | Nåværende prisvurdering | Huidige Prijsbeoordeling | Aktuellt prisbetyg | ❌ |
|
||||
| <span id="ref-next_interval_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`next_interval_price_rating` | Next Price Rating | Nächste Preisbewertung | Neste prisvurdering | Volgende Prijsbeoordeling | Nästa prisbetyg | ❌ |
|
||||
| <span id="ref-previous_interval_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`previous_interval_price_rating` | Previous Price Rating | Vorherige Preisbewertung | Forrige prisvurdering | Vorige Prijsbeoordeling | Föregående prisbetyg | ❌ |
|
||||
| <span id="ref-current_hour_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`current_hour_price_rating` | Current Hour Price Rating | Aktuelle Stunden-Preisbewertung | Nåværende timeprisvurdering | Huidig Uur Prijsbeoordeling | Aktuellt timprisbetyg | ❌ |
|
||||
| <span id="ref-next_hour_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`next_hour_price_rating` | Next Hour Price Rating | Nächste Stunden-Preisbewertung | Neste timeprisvurdering | Volgend Uur Prijsbeoordeling | Nästa timprisbetyg | ❌ |
|
||||
| <span id="ref-yesterday_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`yesterday_price_rating` | Yesterday's Price Rating | Preisbewertung gestern | Prisvurdering i går | Gisteren Prijsbeoordeling | Gårdagens prisbetyg | ❌ |
|
||||
| <span id="ref-today_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`today_price_rating` | Today's Price Rating | Preisbewertung heute | Prisvurdering i dag | Vandaag Prijsbeoordeling | Dagens prisbetyg | ❌ |
|
||||
| <span id="ref-tomorrow_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`tomorrow_price_rating` | Tomorrow's Price Rating | Preisbewertung morgen | Prisvurdering i morgen | Morgen Prijsbeoordeling | Morgondagens prisbetyg | ❌ |
|
||||
| <span id="ref-daily_rating" class="entity-anchor"></span>`daily_rating` | Daily Price Rating | Tägliche Preisbewertung | Daglig prisvurdering | Dagelijkse Prijsbeoordeling | Dagligt prisbetyg | ✅ |
|
||||
| <span id="ref-monthly_rating" class="entity-anchor"></span>`monthly_rating` | Monthly Price Rating | Monatliche Preisbewertung | Månedlig prisvurdering | Maandelijkse Prijsbeoordeling | Månatligt prisbetyg | ✅ |
|
||||
|
||||
### Price Outlook & Trend
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-current_price_trend" class="entity-anchor" data-refs="automation-examples#sensor-combination-quick-reference"></span>`current_price_trend` | Current Price Trend | Aktueller Preistrend | Nåværende pristrend | Huidige Prijstrend | Aktuell pristrend | ✅ |
|
||||
| <span id="ref-next_price_trend_change" class="entity-anchor" data-refs="automation-examples#sensor-combination-quick-reference"></span>`next_price_trend_change` | Next Price Trend Change | Nächste Trendänderung | Neste trendendring | Volgende Prijstrend Wijziging | Nästa pristrendändring | ✅ |
|
||||
| <span id="ref-next_price_trend_change_in" class="entity-anchor"></span>`next_price_trend_change_in` | Next Price Trend Change In | Nächste Trendänderung in | Neste trendendring om | Volgende Prijstrend Wijziging over | Nästa pristrendändring om | ✅ |
|
||||
| <span id="ref-price_outlook_1h" class="entity-anchor"></span>`price_outlook_1h` | Price Outlook (1h) | Preisausblick (1h) | Prisutblikk (1t) | Prijsvooruitzicht (1u) | Prisöversikt (1h) | ✅ |
|
||||
| <span id="ref-price_outlook_2h" class="entity-anchor"></span>`price_outlook_2h` | Price Outlook (2h) | Preisausblick (2h) | Prisutblikk (2t) | Prijsvooruitzicht (2u) | Prisöversikt (2h) | ✅ |
|
||||
| <span id="ref-price_outlook_3h" class="entity-anchor"></span>`price_outlook_3h` | Price Outlook (3h) | Preisausblick (3h) | Prisutblikk (3t) | Prijsvooruitzicht (3u) | Prisöversikt (3h) | ✅ |
|
||||
| <span id="ref-price_outlook_4h" class="entity-anchor"></span>`price_outlook_4h` | Price Outlook (4h) | Preisausblick (4h) | Prisutblikk (4t) | Prijsvooruitzicht (4u) | Prisöversikt (4h) | ✅ |
|
||||
| <span id="ref-price_outlook_5h" class="entity-anchor"></span>`price_outlook_5h` | Price Outlook (5h) | Preisausblick (5h) | Prisutblikk (5t) | Prijsvooruitzicht (5u) | Prisöversikt (5h) | ✅ |
|
||||
| <span id="ref-price_outlook_6h" class="entity-anchor"></span>`price_outlook_6h` | Price Outlook (6h) | Preisausblick (6h) | Prisutblikk (6t) | Prijsvooruitzicht (6u) | Prisöversikt (6h) | ❌ |
|
||||
| <span id="ref-price_outlook_8h" class="entity-anchor"></span>`price_outlook_8h` | Price Outlook (8h) | Preisausblick (8h) | Prisutblikk (8t) | Prijsvooruitzicht (8u) | Prisöversikt (8h) | ❌ |
|
||||
| <span id="ref-price_outlook_12h" class="entity-anchor"></span>`price_outlook_12h` | Price Outlook (12h) | Preisausblick (12h) | Prisutblikk (12t) | Prijsvooruitzicht (12u) | Prisöversikt (12h) | ❌ |
|
||||
| <span id="ref-price_trajectory_2h" class="entity-anchor"></span>`price_trajectory_2h` | Price Trajectory (2h) | Preisverlauf (2h) | Prisforløp (2t) | Prijstrajectorie (2u) | Prisutveckling (2h) | ✅ |
|
||||
| <span id="ref-price_trajectory_3h" class="entity-anchor"></span>`price_trajectory_3h` | Price Trajectory (3h) | Preisverlauf (3h) | Prisforløp (3t) | Prijstrajectorie (3u) | Prisutveckling (3h) | ✅ |
|
||||
| <span id="ref-price_trajectory_4h" class="entity-anchor"></span>`price_trajectory_4h` | Price Trajectory (4h) | Preisverlauf (4h) | Prisforløp (4t) | Prijstrajectorie (4u) | Prisutveckling (4h) | ✅ |
|
||||
| <span id="ref-price_trajectory_5h" class="entity-anchor"></span>`price_trajectory_5h` | Price Trajectory (5h) | Preisverlauf (5h) | Prisforløp (5t) | Prijstrajectorie (5u) | Prisutveckling (5h) | ✅ |
|
||||
| <span id="ref-price_trajectory_6h" class="entity-anchor"></span>`price_trajectory_6h` | Price Trajectory (6h) | Preisverlauf (6h) | Prisforløp (6t) | Prijstrajectorie (6u) | Prisutveckling (6h) | ❌ |
|
||||
| <span id="ref-price_trajectory_8h" class="entity-anchor"></span>`price_trajectory_8h` | Price Trajectory (8h) | Preisverlauf (8h) | Prisforløp (8t) | Prijstrajectorie (8u) | Prisutveckling (8h) | ❌ |
|
||||
| <span id="ref-price_trajectory_12h" class="entity-anchor"></span>`price_trajectory_12h` | Price Trajectory (12h) | Preisverlauf (12h) | Prisforløp (12t) | Prijstrajectorie (12u) | Prisutveckling (12h) | ❌ |
|
||||
|
||||
### Volatility Sensors
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-today_volatility" class="entity-anchor" data-refs="automation-examples#sensor-combination-quick-reference,sensors-volatility#available-volatility-sensors"></span>`today_volatility` | Today's Price Volatility | Volatilität heute | Volatilitet i dag | Vandaag Prijsvolatiliteit | Dagens prisvolatilitet | ✅ |
|
||||
| <span id="ref-tomorrow_volatility" class="entity-anchor" data-refs="sensors-volatility#available-volatility-sensors"></span>`tomorrow_volatility` | Tomorrow's Price Volatility | Volatilität morgen | Volatilitet i morgen | Morgen Prijsvolatiliteit | Morgondagens prisvolatilitet | ❌ |
|
||||
| <span id="ref-next_24h_volatility" class="entity-anchor"></span>`next_24h_volatility` | Next 24h Price Volatility | Volatilität der nächsten 24h | Volatilitet neste 24t | Komende 24u Prijsvolatiliteit | Nästa 24h prisvolatilitet | ❌ |
|
||||
| <span id="ref-today_tomorrow_volatility" class="entity-anchor" data-refs="sensors-volatility#available-volatility-sensors"></span>`today_tomorrow_volatility` | Today+Tomorrow Price Volatility | Volatilität heute+morgen | Volatilitet i dag+i morgen | Vandaag+Morgen Prijsvolatiliteit | Idag+Imorgon prisvolatilitet | ❌ |
|
||||
|
||||
### Best Price Timing
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-best_price_end_time" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_end_time` | Best Price End | Bestpreis endet | Beste pris slutter | Beste Prijs Einde | Bästa pris slutar | ✅ |
|
||||
| <span id="ref-best_price_period_duration" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_period_duration` | Best Price Duration | Bestpreis Dauer | Beste pris varighet | Beste Prijs Duur | Bästa pris varaktighet | ❌ |
|
||||
| <span id="ref-best_price_remaining_minutes" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_remaining_minutes` | Best Price Remaining Time | Bestpreis verbleibend | Beste pris gjenværende tid | Beste Prijs Resterende Tijd | Bästa pris återstående tid | ✅ |
|
||||
| <span id="ref-best_price_progress" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_progress` | Best Price Progress | Bestpreis Fortschritt | Beste pris fremgang | Beste Prijs Voortgang | Bästa pris framsteg | ✅ |
|
||||
| <span id="ref-best_price_next_start_time" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_next_start_time` | Best Price Start | Bestpreis startet | Beste pris starter | Beste Prijs Start | Bästa pris startar | ✅ |
|
||||
| <span id="ref-best_price_next_in_minutes" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_next_in_minutes` | Best Price Starts In | Bestpreis startet in | Beste pris starter om | Beste Prijs Start Over | Bästa pris startar om | ✅ |
|
||||
|
||||
### Peak Price Timing
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-peak_price_end_time" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_end_time` | Peak Price End | Spitzenpreis endet | Topppris slutter | Piekprijs Einde | Topppris slutar | ✅ |
|
||||
| <span id="ref-peak_price_period_duration" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_period_duration` | Peak Price Duration | Spitzenpreis Dauer | Topppris varighet | Piekprijs Duur | Topppris varaktighet | ❌ |
|
||||
| <span id="ref-peak_price_remaining_minutes" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_remaining_minutes` | Peak Price Remaining Time | Spitzenpreis verbleibend | Topppris gjenværende tid | Piekprijs Resterende Tijd | Topppris återstående tid | ✅ |
|
||||
| <span id="ref-peak_price_progress" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_progress` | Peak Price Progress | Spitzenpreis Fortschritt | Topppris fremgang | Piekprijs Voortgang | Topppris framsteg | ✅ |
|
||||
| <span id="ref-peak_price_next_start_time" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_next_start_time` | Peak Price Start | Spitzenpreis startet | Topppris starter | Piekprijs Start | Topppris startar | ✅ |
|
||||
| <span id="ref-peak_price_next_in_minutes" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_next_in_minutes` | Peak Price Starts In | Spitzenpreis startet in | Topppris starter om | Piekprijs Start Over | Topppris startar om | ✅ |
|
||||
|
||||
### Home & Metering Metadata
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-home_type" class="entity-anchor"></span>`home_type` | Home Type | Wohnungstyp | Boligtype | Huistype | Hemtyp | ❌ |
|
||||
| <span id="ref-home_size" class="entity-anchor"></span>`home_size` | Home Size | Wohnfläche | Boligareal | Huisgrootte | Hemstorlek | ❌ |
|
||||
| <span id="ref-main_fuse_size" class="entity-anchor"></span>`main_fuse_size` | Main Fuse Size | Hauptsicherung | Hovedsikring | Hoofdzekering Grootte | Huvudsäkringsstorlek | ❌ |
|
||||
| <span id="ref-number_of_residents" class="entity-anchor"></span>`number_of_residents` | Number of Residents | Anzahl Bewohner | Antall beboere | Aantal Bewoners | Antal boende | ❌ |
|
||||
| <span id="ref-primary_heating_source" class="entity-anchor"></span>`primary_heating_source` | Primary Heating Source | Primäre Heizquelle | Primær varmekilde | Primaire Verwarmingsbron | Primär värmekälla | ❌ |
|
||||
| <span id="ref-grid_company" class="entity-anchor"></span>`grid_company` | Grid Company | Netzbetreiber | Nettselskap | Netbedrijf | Nätbolag | ✅ |
|
||||
| <span id="ref-grid_area_code" class="entity-anchor"></span>`grid_area_code` | Grid Area Code | Netzgebietscode | Nettområdekode | Netgebiedcode | Nätområdeskod | ❌ |
|
||||
| <span id="ref-price_area_code" class="entity-anchor"></span>`price_area_code` | Price Area Code | Preiszonencode | Prisområdekode | Prijsgebiedcode | Prisområdeskod | ❌ |
|
||||
| <span id="ref-consumption_ean" class="entity-anchor"></span>`consumption_ean` | Consumption EAN | Verbrauchs-EAN | Forbruks-EAN | Verbruik EAN | Förbruknings-EAN | ❌ |
|
||||
| <span id="ref-production_ean" class="entity-anchor"></span>`production_ean` | Production EAN | Erzeugungs-EAN | Produksjons-EAN | Productie EAN | Produktions-EAN | ❌ |
|
||||
| <span id="ref-energy_tax_type" class="entity-anchor"></span>`energy_tax_type` | Energy Tax Type | Energiesteuertyp | Energiavgiftstype | Energiebelasting Type | Energiskattetyp | ❌ |
|
||||
| <span id="ref-vat_type" class="entity-anchor"></span>`vat_type` | VAT Type | Mehrwertsteuertyp | MVA-type | BTW Type | Momstyp | ❌ |
|
||||
| <span id="ref-estimated_annual_consumption" class="entity-anchor"></span>`estimated_annual_consumption` | Estimated Annual Consumption | Geschätzter Jahresverbrauch | Estimert årlig forbruk | Geschat Jaarverbruik | Beräknad årlig förbrukning | ✅ |
|
||||
| <span id="ref-subscription_status" class="entity-anchor"></span>`subscription_status` | Subscription Status | Abonnementstatus | Abonnementsstatus | Abonnement Status | Abonnemangsstatus | ❌ |
|
||||
|
||||
### Data & Diagnostics
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-data_lifecycle_status" class="entity-anchor"></span>`data_lifecycle_status` | Data Lifecycle Status | Datenlebenszyklus-Status | Datalivssyklus-status | Data Levenscyclus Status | Datalivscykelstatus | ✅ |
|
||||
| <span id="ref-chart_data_export" class="entity-anchor"></span>`chart_data_export` | Chart Data Export | Diagramm-Datenexport | Diagramdataeksport | Grafiekdata Export | Diagramdataexport | ❌ |
|
||||
| <span id="ref-chart_metadata" class="entity-anchor"></span>`chart_metadata` | Chart Metadata | Diagramm-Metadaten | Diagrammetadata | Grafiek Metadata | Diagrammetadata | ✅ |
|
||||
## Binary Sensors
|
||||
|
||||
### Binary Sensors
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-best_price_period" class="entity-anchor" data-refs="period-calculation#what-are-price-periods,sensors-overview#best-price-period-peak-price-period"></span>`best_price_period` | Best Price Period | Bestpreis-Zeitraum | Lavpris-periode | Beste Prijs Periode | Bästa Prisperiod | ✅ |
|
||||
| <span id="ref-peak_price_period" class="entity-anchor" data-refs="period-calculation#what-are-price-periods,sensors-overview#best-price-period-peak-price-period"></span>`peak_price_period` | Peak Price Period | Spitzenpreis-Zeitraum | Toppris-periode | Piekprijs Periode | Topprisperiod | ✅ |
|
||||
| <span id="ref-connection" class="entity-anchor"></span>`connection` | Tibber API Connection | Tibber-API-Verbindung | Tibber API-tilkobling | Tibber API Verbinding | Tibber API-anslutning | ✅ |
|
||||
| <span id="ref-tomorrow_data_available" class="entity-anchor"></span>`tomorrow_data_available` | Tomorrow's Data Available | Morgige Daten verfügbar | Morgendagens data tilgjengelig | Morgen Gegevens Beschikbaar | Morgondagens data tillgänglig | ✅ |
|
||||
| <span id="ref-has_ventilation_system" class="entity-anchor"></span>`has_ventilation_system` | Has Ventilation System | Hat Lüftungsanlage | Har ventilasjonsanlegg | Heeft Ventilatiesysteem | Har ventilationssystem | ❌ |
|
||||
| <span id="ref-realtime_consumption_enabled" class="entity-anchor"></span>`realtime_consumption_enabled` | Realtime Consumption Enabled | Echtzeitverbrauch aktiviert | Sanntidsforbruk aktivert | Realtime Verbruik Ingeschakeld | Realtidsförbrukning aktiverad | ❌ |
|
||||
## Number Entities (Configuration Overrides)
|
||||
|
||||
> These entities allow runtime adjustment of period calculation parameters without changing the integration configuration. All are **disabled by default**.
|
||||
|
||||
### Best Price Configuration
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-best_price_flex_override" class="entity-anchor" data-refs="configuration#best-price-period-settings"></span>`best_price_flex_override` | Best Price: Flexibility | Bestpreis: Flexibilität | Beste pris: Fleksibilitet | Beste prijs: Flexibiliteit | Bästa pris: Flexibilitet | ❌ |
|
||||
| <span id="ref-best_price_min_distance_override" class="entity-anchor" data-refs="configuration#best-price-period-settings"></span>`best_price_min_distance_override` | Best Price: Minimum Distance | Bestpreis: Mindestabstand | Beste pris: Minimumsavstand | Beste prijs: Minimale afstand | Bästa pris: Minimiavstånd | ❌ |
|
||||
| <span id="ref-best_price_min_period_length_override" class="entity-anchor" data-refs="configuration#best-price-period-settings"></span>`best_price_min_period_length_override` | Best Price: Minimum Period Length | Bestpreis: Mindestperiodenlänge | Beste pris: Minimum periodelengde | Beste prijs: Minimale periodelengte | Bästa pris: Minsta periodlängd | ❌ |
|
||||
| <span id="ref-best_price_min_periods_override" class="entity-anchor" data-refs="configuration#best-price-period-settings"></span>`best_price_min_periods_override` | Best Price: Minimum Periods | Bestpreis: Mindestperioden | Beste pris: Minimum perioder | Beste prijs: Minimum periodes | Bästa pris: Minsta antal perioder | ❌ |
|
||||
| <span id="ref-best_price_relaxation_attempts_override" class="entity-anchor" data-refs="configuration#best-price-period-settings"></span>`best_price_relaxation_attempts_override` | Best Price: Relaxation Attempts | Bestpreis: Lockerungsversuche | Beste pris: Lemping forsøk | Beste prijs: Versoepeling pogingen | Bästa pris: Lättnadsförsök | ❌ |
|
||||
| <span id="ref-best_price_gap_count_override" class="entity-anchor"></span>`best_price_gap_count_override` | Best Price: Gap Tolerance | Bestpreis: Lückentoleranz | Beste pris: Gaptoleranse | Beste prijs: Gap tolerantie | Bästa pris: Glaptolerans | ❌ |
|
||||
|
||||
### Peak Price Configuration
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-peak_price_flex_override" class="entity-anchor" data-refs="configuration#peak-price-period-settings"></span>`peak_price_flex_override` | Peak Price: Flexibility | Spitzenpreis: Flexibilität | Topppris: Fleksibilitet | Piekprijs: Flexibiliteit | Topppris: Flexibilitet | ❌ |
|
||||
| <span id="ref-peak_price_min_distance_override" class="entity-anchor" data-refs="configuration#peak-price-period-settings"></span>`peak_price_min_distance_override` | Peak Price: Minimum Distance | Spitzenpreis: Mindestabstand | Topppris: Minimumsavstand | Piekprijs: Minimale afstand | Topppris: Minimiavstånd | ❌ |
|
||||
| <span id="ref-peak_price_min_period_length_override" class="entity-anchor" data-refs="configuration#peak-price-period-settings"></span>`peak_price_min_period_length_override` | Peak Price: Minimum Period Length | Spitzenpreis: Mindestperiodenlänge | Topppris: Minimum periodelengde | Piekprijs: Minimale periodelengte | Topppris: Minsta periodlängd | ❌ |
|
||||
| <span id="ref-peak_price_min_periods_override" class="entity-anchor" data-refs="configuration#peak-price-period-settings"></span>`peak_price_min_periods_override` | Peak Price: Minimum Periods | Spitzenpreis: Mindestperioden | Topppris: Minimum perioder | Piekprijs: Minimum periodes | Topppris: Minsta antal perioder | ❌ |
|
||||
| <span id="ref-peak_price_relaxation_attempts_override" class="entity-anchor" data-refs="configuration#peak-price-period-settings"></span>`peak_price_relaxation_attempts_override` | Peak Price: Relaxation Attempts | Spitzenpreis: Lockerungsversuche | Topppris: Lemping forsøk | Piekprijs: Versoepeling pogingen | Topppris: Lättnadsförsök | ❌ |
|
||||
| <span id="ref-peak_price_gap_count_override" class="entity-anchor"></span>`peak_price_gap_count_override` | Peak Price: Gap Tolerance | Spitzenpreis: Lückentoleranz | Topppris: Gaptoleranse | Piekprijs: Gap tolerantie | Topppris: Glaptolerans | ❌ |
|
||||
## Switch Entities (Configuration Overrides)
|
||||
|
||||
> These switches control whether the relaxation algorithm is active for period detection. All are **disabled by default**.
|
||||
|
||||
### Switches
|
||||
|
||||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-best_price_enable_relaxation_override" class="entity-anchor"></span>`best_price_enable_relaxation_override` | Best Price: Achieve Minimum Count | Bestpreis: Mindestanzahl erreichen | Beste pris: Oppnå minimumsantall | Beste prijs: Minimum aantal bereiken | Bästa pris: Uppnå minimiantal | ❌ |
|
||||
| <span id="ref-peak_price_enable_relaxation_override" class="entity-anchor"></span>`peak_price_enable_relaxation_override` | Peak Price: Achieve Minimum Count | Spitzenpreis: Mindestanzahl erreichen | Topppris: Oppnå minimumsantall | Piekprijs: Minimum aantal bereiken | Topppris: Uppnå minimiantal | ❌ |
|
||||
194
docs/user/docs/sensors-average.md
Normal file
194
docs/user/docs/sensors-average.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Average & Statistics 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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
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 |
|
||||
|--------|-------------|-------------|
|
||||
| <EntityRef id="average_price_today">Average Price Today</EntityRef> | Typical price for current calendar day | 00:00 - 23:59 today |
|
||||
| <EntityRef id="average_price_tomorrow">Average Price Tomorrow</EntityRef> | Typical price for next calendar day | 00:00 - 23:59 tomorrow |
|
||||
| <EntityRef id="trailing_price_average">Trailing Price Average</EntityRef> | Typical price for last 24 hours | Rolling 24h backward |
|
||||
| <EntityRef id="leading_price_average">Leading Price Average</EntityRef> | Typical price for next 24 hours | Rolling 24h forward |
|
||||
| <EntityRef id="current_hour_average_price">Current Hour Average</EntityRef> | Smoothed price around current time | 5 intervals (~75 min) |
|
||||
| <EntityRef id="next_hour_average_price">Next Hour Average</EntityRef> | Smoothed price around next hour | 5 intervals (~75 min) |
|
||||
| **Next N Hours Average** (`next_avg_1h`–`next_avg_12h`) | 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:
|
||||
|
||||
<details>
|
||||
<summary>Show YAML: Automation — start dishwasher when cheap</summary>
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**Example 2: Cost-aware heating control**
|
||||
|
||||
Use mean for actual cost calculations:
|
||||
|
||||
<details>
|
||||
<summary>Show YAML: Automation — cost-aware heating control</summary>
|
||||
|
||||
```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)"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**Example 3: Smart charging based on rolling average**
|
||||
|
||||
Use trailing average to understand recent price trends:
|
||||
|
||||
<details>
|
||||
<summary>Show YAML: Automation — EV charging based on rolling average</summary>
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 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)
|
||||
110
docs/user/docs/sensors-energy-tax.md
Normal file
110
docs/user/docs/sensors-energy-tax.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Energy & Tax Attributes
|
||||
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
Most price sensors include **energy price** and **tax** attributes that break down the total price into its components:
|
||||
|
||||
```
|
||||
total = energy_price + tax
|
||||
```
|
||||
|
||||
These attributes appear on **all price sensors that display a raw price** (not on percentage, level, or trend sensors). The `energy_price` is the raw spot/market price, while `tax` includes all fees, surcharges, and taxes added by your electricity provider.
|
||||
|
||||
:::note Transition After Update
|
||||
After updating the integration, the `energy_price` and `tax` attributes will appear gradually as new price data is fetched from the Tibber API. Existing cached intervals (up to ~2 days old) won't have these fields yet — the attributes will simply be absent until fresh data replaces them. No action needed.
|
||||
:::
|
||||
|
||||
## Where These Attributes Appear
|
||||
|
||||
| Sensor Type | `energy_price` | `tax` | Notes |
|
||||
|-------------|:-:|:-:|-------|
|
||||
| Current/Next/Previous Interval Price | ✅ | ✅ | Single interval values |
|
||||
| Rolling Hour Average | ✅ | ✅ | Averaged across 5 intervals |
|
||||
| Daily Min/Max/Average | ✅ | ✅ | Aggregated for the day |
|
||||
| Trailing/Leading 24h | ✅ | ✅ | Aggregated across window |
|
||||
| Future Average (N-hour) | ✅ | ✅ | Averaged across future window |
|
||||
| Levels, Ratings, Trends | ❌ | ❌ | Not price sensors |
|
||||
| Volatility | ❌ | ❌ | Statistical, not price |
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Solar Feed-In & Net Metering (Saldering)
|
||||
|
||||
In countries like the Netherlands, solar feed-in compensation is based on the **raw energy/spot price**, not the total consumer price. The `energy_price` attribute gives you exactly this value — no more reverse-engineering from the total price with fragile template calculations.
|
||||
|
||||
<details>
|
||||
<summary>Show YAML: Automation — solar export or consume decision</summary>
|
||||
|
||||
```yaml
|
||||
# Example: Decide whether to export solar power or consume it
|
||||
# Compare energy price (what you'd earn by exporting) vs. total price (what you'd pay)
|
||||
automation:
|
||||
- alias: "Solar: Export or Consume"
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.solar_production_power
|
||||
above: 2000 # Producing more than 2 kW
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set energy = state_attr('sensor.<home_name>_current_electricity_price', 'energy_price') %}
|
||||
{% set total = states('sensor.<home_name>_current_electricity_price') | float %}
|
||||
{# Export when energy price is high relative to total — you earn more #}
|
||||
{{ energy is not none and energy > (total * 0.4) }}
|
||||
action:
|
||||
- service: switch.turn_off
|
||||
entity_id: switch.battery_charging # Don't charge battery, export instead
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Price Composition Analysis
|
||||
|
||||
Understand how your electricity price is structured — useful for comparing across days or spotting trends in market prices vs. fees:
|
||||
|
||||
<details>
|
||||
<summary>Show YAML: Template sensor — electricity tax share percentage</summary>
|
||||
|
||||
```yaml
|
||||
# Template sensor showing tax share
|
||||
template:
|
||||
- sensor:
|
||||
- name: "Electricity Tax Share"
|
||||
unit_of_measurement: "%"
|
||||
state: >
|
||||
{% set tax = state_attr('sensor.<home_name>_current_electricity_price', 'tax') %}
|
||||
{% set total = states('sensor.<home_name>_current_electricity_price') | float %}
|
||||
{% if tax is not none and total > 0 %}
|
||||
{{ ((tax / total) * 100) | round(1) }}
|
||||
{% else %}
|
||||
unavailable
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Dashboard: Daily Cost Breakdown
|
||||
|
||||
Show users how today's average price splits into energy vs. tax:
|
||||
|
||||
```yaml
|
||||
# Mushroom chips card showing the split
|
||||
type: custom:mushroom-chips-card
|
||||
chips:
|
||||
- type: template
|
||||
icon: mdi:flash
|
||||
content: >
|
||||
⚡ {{ state_attr('sensor.<home_name>_price_today', 'energy_price_mean') | round(1) }} ct
|
||||
- type: template
|
||||
icon: mdi:receipt-text
|
||||
content: >
|
||||
🏛️ {{ state_attr('sensor.<home_name>_price_today', 'tax_mean') | round(1) }} ct
|
||||
```
|
||||
|
||||
## Country-Specific Calculations
|
||||
|
||||
The composition of the `tax` field varies by country (Norway, Sweden, Germany, Netherlands each have different fee structures). For detailed examples of how to build country-specific calculations using `input_number` helpers and template sensors — including **Dutch solar feed-in compensation (saldering)** — see the **[Community Examples](community-examples.md#country-specific-price-calculations)** page.
|
||||
|
||||
## In Chart Data Actions
|
||||
|
||||
The `energy_price` and `tax` fields are also available in the `get_chartdata` action. See [Actions — Energy & Tax Fields](./actions.md#energy--tax-fields-in-get_chartdata) for details.
|
||||
183
docs/user/docs/sensors-overview.md
Normal file
183
docs/user/docs/sensors-overview.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# Sensors Overview
|
||||
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
The integration provides **100+ sensors** organized by purpose. This page gives a quick overview and links to detailed guides for each sensor family.
|
||||
|
||||
| Sensor Family | Purpose | Guide |
|
||||
|---|---|---|
|
||||
| **Binary Sensors** | Period on/off indicators | [below](#binary-sensors) |
|
||||
| **Core Price** | Current, next, previous interval prices | [below](#core-price-sensors) |
|
||||
| **Average & Statistics** | Daily averages, rolling averages, median/mean | [Average Sensors](sensors-average.md) |
|
||||
| **Ratings & Levels** | Price classification (3-level ratings, 5-level API levels) | [Ratings & Levels](sensors-ratings-levels.md) |
|
||||
| **Min/Max** | Daily and rolling 24h extremes | [below](#minmax-sensors) |
|
||||
| **Volatility** | Price fluctuation analysis | [Volatility Sensors](sensors-volatility.md) |
|
||||
| **Trends** | Price outlook, trajectory, direction | [Trend Sensors](sensors-trends.md) |
|
||||
| **Timing** | Period countdown, progress, duration | [Timing Sensors](sensors-timing.md) |
|
||||
| **Energy & Tax** | Spot price and tax breakdown | [Energy & Tax](sensors-energy-tax.md) |
|
||||
| **Diagnostic** | Chart metadata, data export | [below](#diagnostic-sensors) |
|
||||
|
||||
---
|
||||
|
||||
## 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:**
|
||||
|
||||
- <EntityRef id="best_price_period">Best Price Period</EntityRef>: Turns ON during periods with significantly lower prices than the daily average
|
||||
- <EntityRef id="peak_price_period">Peak Price Period</EntityRef>: 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
|
||||
|
||||
The integration provides price sensors for the **current**, **next**, and **previous** 15-minute interval. Each exposes the total price as sensor state, with `energy_price` and `tax` available as attributes (see [Energy & Tax Breakdown](sensors-energy-tax.md)).
|
||||
|
||||
**Next N Hours Average** sensors (`next_avg_1h`–`next_avg_12h`) provide future price forecasts for 1h, 2h, 3h, 4h, 5h, 6h, 8h, and 12h windows.
|
||||
|
||||
For detailed average sensor behavior (median vs mean, configuration, automation examples), see **[Average & Statistics Sensors](sensors-average.md)**.
|
||||
|
||||
## Min/Max Sensors
|
||||
|
||||
These sensors show the lowest and highest prices for calendar days and rolling windows:
|
||||
|
||||
### Daily Min/Max
|
||||
|
||||
| Sensor | Description |
|
||||
|--------|-------------|
|
||||
| <EntityRef id="lowest_price_today">Today's Lowest Price</EntityRef> | Minimum price today (00:00–23:59) |
|
||||
| <EntityRef id="highest_price_today">Today's Highest Price</EntityRef> | Maximum price today (00:00–23:59) |
|
||||
| <EntityRef id="lowest_price_tomorrow">Tomorrow's Lowest Price</EntityRef> | Minimum price tomorrow |
|
||||
| <EntityRef id="highest_price_tomorrow">Tomorrow's Highest Price</EntityRef> | Maximum price tomorrow |
|
||||
|
||||
### 24-Hour Rolling Min/Max
|
||||
|
||||
| Sensor | Description |
|
||||
|--------|-------------|
|
||||
| <EntityRef id="trailing_price_min">Trailing Price Min</EntityRef> | Lowest price in the last 24 hours |
|
||||
| <EntityRef id="trailing_price_max">Trailing Price Max</EntityRef> | Highest price in the last 24 hours |
|
||||
| <EntityRef id="leading_price_min">Leading Price Min</EntityRef> | Lowest price in the next 24 hours |
|
||||
| <EntityRef id="leading_price_max">Leading Price Max</EntityRef> | 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 |
|
||||
|
||||
## 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`** action 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 action 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` action documentation for a complete list of available parameters. All action 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 action:
|
||||
|
||||
```yaml
|
||||
# Old approach (sensor)
|
||||
- service: apexcharts_card.update
|
||||
data:
|
||||
entity: sensor.<home_name>_chart_data_export
|
||||
|
||||
# New approach (action)
|
||||
- service: tibber_prices.get_chartdata
|
||||
data:
|
||||
entry_id: YOUR_CONFIG_ENTRY_ID
|
||||
day: ["today", "tomorrow"]
|
||||
output_format: array_of_objects
|
||||
response_variable: chart_data
|
||||
```
|
||||
140
docs/user/docs/sensors-ratings-levels.md
Normal file
140
docs/user/docs/sensors-ratings-levels.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Ratings & Levels
|
||||
|
||||
> **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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
The integration provides **two** classification systems for electricity prices. Both are useful, but serve different purposes.
|
||||
|
||||
## Ratings vs Levels at a Glance
|
||||
|
||||
| | 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")
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|--------|-------|-------------|
|
||||
| <EntityRef id="current_interval_price_rating">Current Price Rating</EntityRef> | Current interval | Rating of the current 15-minute price |
|
||||
| <EntityRef id="next_interval_price_rating">Next Price Rating</EntityRef> | Next interval | Rating for the upcoming 15-minute price |
|
||||
| <EntityRef id="previous_interval_price_rating">Previous Price Rating</EntityRef> | Previous interval | Rating for the past 15-minute price |
|
||||
| <EntityRef id="current_hour_price_rating">Current Hour Price Rating</EntityRef> | Rolling 5-interval | Smoothed rating around the current hour |
|
||||
| <EntityRef id="next_hour_price_rating">Next Hour Price Rating</EntityRef> | Rolling 5-interval | Smoothed rating around the next hour |
|
||||
| <EntityRef id="yesterday_price_rating">Yesterday's Price Rating</EntityRef> | Calendar day | Aggregated rating for yesterday |
|
||||
| <EntityRef id="today_price_rating">Today's Price Rating</EntityRef> | Calendar day | Aggregated rating for today |
|
||||
| <EntityRef id="tomorrow_price_rating">Tomorrow's Price Rating</EntityRef> | Calendar day | Aggregated rating for tomorrow |
|
||||
|
||||
### 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 |
|
||||
|--------|-------|
|
||||
| <EntityRef id="current_interval_price_level">Current Price Level</EntityRef> | Current interval |
|
||||
| <EntityRef id="next_interval_price_level">Next Price Level</EntityRef> | Next interval |
|
||||
| <EntityRef id="previous_interval_price_level">Previous Price Level</EntityRef> | Previous interval |
|
||||
| <EntityRef id="current_hour_price_level">Current Hour Price Level</EntityRef> | Rolling 5-interval window |
|
||||
| <EntityRef id="next_hour_price_level">Next Hour Price Level</EntityRef> | Rolling 5-interval window |
|
||||
| <EntityRef id="yesterday_price_level">Yesterday's Price Level</EntityRef> | Calendar day (aggregated) |
|
||||
| <EntityRef id="today_price_level">Today's Price Level</EntityRef> | Calendar day (aggregated) |
|
||||
| <EntityRef id="tomorrow_price_level">Tomorrow's Price Level</EntityRef> | 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).
|
||||
95
docs/user/docs/sensors-timing.md
Normal file
95
docs/user/docs/sensors-timing.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Timing 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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
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` 0–100% 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 |
|
||||
|--------|-------------------|----------------------|
|
||||
| <EntityRef id="best_price_end_time" also="peak_price_end_time">End Time</EntityRef> | Current period's end time | Next period's end time |
|
||||
| <EntityRef id="best_price_period_duration" also="peak_price_period_duration">Period Duration</EntityRef> | Current period length (minutes) | Next period length |
|
||||
| <EntityRef id="best_price_remaining_minutes" also="peak_price_remaining_minutes">Remaining Minutes</EntityRef> | Minutes until current period ends | 0 |
|
||||
| <EntityRef id="best_price_progress" also="peak_price_progress">Progress</EntityRef> | 0–100% through current period | 0 |
|
||||
| <EntityRef id="best_price_next_start_time" also="peak_price_next_start_time">Next Start Time</EntityRef> | When next-next period starts | When next period starts |
|
||||
| <EntityRef id="best_price_next_in_minutes" also="peak_price_next_in_minutes">Next In Minutes</EntityRef> | 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
|
||||
|
||||
<details>
|
||||
<summary>Show YAML: Bar card for period progress</summary>
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Notify When Period Is Almost Over
|
||||
|
||||
<details>
|
||||
<summary>Show YAML: Automation — notify when best price period is ending</summary>
|
||||
|
||||
```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!"
|
||||
```
|
||||
|
||||
</details>
|
||||
295
docs/user/docs/sensors-trends.md
Normal file
295
docs/user/docs/sensors-trends.md
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
# Trend 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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
Trend sensors help you understand **whether to act now or wait**. The integration provides two complementary families:
|
||||
|
||||
- **Price Outlook Sensors (1h–12h):** Compare current price vs. future window average — "Is now cheaper than the next Nh on average?"
|
||||
- **Price Trajectory Sensors (2h–12h):** Compare first half vs. second half of the window — "Are prices rising or falling *within* the window?"
|
||||
|
||||
---
|
||||
|
||||
## Price Outlook Sensors (1h–12h)
|
||||
|
||||
These sensors compare the **current price** with the **average price** of the next N hours:
|
||||
|
||||
| Sensor | Compares Against |
|
||||
|--------|-----------------|
|
||||
| **Price Outlook (1h)** (`price_outlook_1h`) | Average of next 1 hour |
|
||||
| **Price Outlook (2h)** (`price_outlook_2h`) | Average of next 2 hours |
|
||||
| **Price Outlook (3h)** (`price_outlook_3h`) | Average of next 3 hours |
|
||||
| **Price Outlook (4h)** (`price_outlook_4h`) | Average of next 4 hours |
|
||||
| **Price Outlook (5h)** (`price_outlook_5h`) | Average of next 5 hours |
|
||||
| **Price Outlook (6h)** (`price_outlook_6h`) | Average of next 6 hours |
|
||||
| **Price Outlook (8h)** (`price_outlook_8h`) | Average of next 8 hours |
|
||||
| **Price Outlook (12h)** (`price_outlook_12h`) | Average of next 12 hours |
|
||||
|
||||
:::info Same Starting Point — All Outlook Sensors Use Your Current Price
|
||||
All outlook sensors share the **same base: your current 15-minute price**. They differ only in how far ahead they average. The windows **overlap** — the 3h average includes ALL intervals from the 1h and 2h windows, plus one more hour.
|
||||
|
||||
**This means:**
|
||||
- `price_outlook_3h` shows "current price vs. average of the **entire** next 3 hours" — **not** "what happens between hour 2 and hour 3"
|
||||
- If 1h shows `falling` but 6h shows `rising`: near-term prices are below your current price, but looking at the full 6h window (which includes expensive evening hours), the overall average is above your current price
|
||||
- Larger windows smooth out short-term fluctuations — a 30-minute price spike affects the 1h average more than the 6h average
|
||||
|
||||
**⚠️ At a price minimum, outlook sensors can be misleading!** If you're at the minimum and prices are about to rise, `price_outlook_3h` may still show `strongly_falling` because the cheap minimum pulls the 3h average below your current high price. Use `price_trajectory_3h` to see the direction *within* the window.
|
||||
:::
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Price Trajectory Sensors (2h–12h)
|
||||
|
||||
These sensors compare the **first half** of the future window against the **second half** — revealing the price *direction within* the window.
|
||||
|
||||
| Sensor | Compares |
|
||||
|--------|----------|
|
||||
| **Price Trajectory (2h)** (`price_trajectory_2h`) | Avg of hour 1 vs avg of hour 2 |
|
||||
| **Price Trajectory (3h)** (`price_trajectory_3h`) | Avg of first 1.5h vs avg of second 1.5h |
|
||||
| **Price Trajectory (4h)** (`price_trajectory_4h`) | Avg of first 2h vs avg of second 2h |
|
||||
| **Price Trajectory (5h)** (`price_trajectory_5h`) | Avg of first 2.5h vs avg of second 2.5h |
|
||||
| **Price Trajectory (6h)** (`price_trajectory_6h`) | Avg of first 3h vs avg of second 3h |
|
||||
| **Price Trajectory (8h)** (`price_trajectory_8h`) | Avg of first 4h vs avg of second 4h |
|
||||
| **Price Trajectory (12h)** (`price_trajectory_12h`) | Avg of first 6h vs avg of second 6h |
|
||||
|
||||
**States:** Same 5-level scale as outlook sensors (`strongly_falling` → `strongly_rising`).
|
||||
|
||||
:::info Why trajectory sensors complement outlook sensors
|
||||
**At a price minimum** — the exact moment you should act — `price_outlook_3h` may show `strongly_falling` because the cheap minimum pulls the entire 3h average below your current high price. But `price_trajectory_3h` shows `rising` because the second half (after the minimum) is more expensive than the first half.
|
||||
|
||||
| Combination | Interpretation |
|
||||
|-------------|----------------|
|
||||
| Outlook `falling` + Trajectory `rising` | **You're AT the minimum** — act now |
|
||||
| Outlook `falling` + Trajectory `falling` | Prices still dropping — wait |
|
||||
| Outlook `rising` + Trajectory `rising` | Strong signal to act now |
|
||||
| Outlook `rising` + Trajectory `falling` | Short spike, then cheaper — wait |
|
||||
:::
|
||||
|
||||
**Key attributes:**
|
||||
|
||||
| Attribute | Description | Example |
|
||||
|-----------|-------------|---------|
|
||||
| `trend_value` | Numeric value for automations (-2 to +2) | `1` |
|
||||
| `first_half_avg` | Average price in first half of window | `12.4` |
|
||||
| `second_half_avg` | Average price in second half of window | `18.1` |
|
||||
| `half_diff_%` | Percentage difference (second vs first half) | `46.0` |
|
||||
|
||||
---
|
||||
|
||||
## 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: 2–6) 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` |
|
||||
|
||||
---
|
||||
|
||||
## Next Price Trend Change In (Countdown)
|
||||
|
||||
**Entity ID:** `sensor.<home_name>_next_price_trend_change_in`
|
||||
|
||||
A **countdown timer** companion to the Next Price Trend Change sensor above. Instead of a timestamp, it shows **how many minutes** remain until the trend changes direction.
|
||||
|
||||
**State:** Duration in minutes until the next trend change (displayed in hours via HA unit conversion). Unavailable if no change is predicted.
|
||||
|
||||
**Use cases:**
|
||||
- Dashboard countdown: "Trend changes in 1.5 h"
|
||||
- Automation trigger: "If trend change is less than 15 minutes away, prepare for price direction change"
|
||||
|
||||
**Example automation:**
|
||||
|
||||
```yaml
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.<home_name>_next_price_trend_change_in
|
||||
below: 0.25 # 15 minutes (displayed in hours)
|
||||
action:
|
||||
- service: notify.mobile_app
|
||||
data:
|
||||
message: "Price trend is about to change direction!"
|
||||
```
|
||||
|
||||
**Tip:** Use this sensor for "HOW LONG" and the Next Price Trend Change sensor (timestamp) for "WHEN".
|
||||
|
||||
---
|
||||
|
||||
## 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 outlook sensor is enough:
|
||||
|
||||
```yaml
|
||||
# Example: Start dishwasher when prices are favorable
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: sensor.my_home_price_outlook_3h
|
||||
condition:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.my_home_price_outlook_3h
|
||||
attribute: trend_value
|
||||
# rising (1) or strongly_rising (2) = act now
|
||||
above: 0
|
||||
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 Outlook | 6h Outlook | Interpretation | Recommendation |
|
||||
|----------|----------|---------------|----------------|
|
||||
| `rising` | `rising` | Prices going up across the board | **Start now** |
|
||||
| `falling` | `falling` | Prices dropping across the board | **Wait** |
|
||||
| `falling` | `rising` | Brief dip, then expensive evening | **Wait briefly**, then start during the dip |
|
||||
| `rising` | `falling` | Short spike, but cheaper hours ahead | **Wait** if you can — better prices coming |
|
||||
| `stable` | any | Short-term doesn't matter | Use the **longer window** for your decision |
|
||||
|
||||
### Dashboard Quick-Glance
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## Outlook & Trajectory vs Average Sensors
|
||||
|
||||
Both sensor families provide future price information, but serve different purposes:
|
||||
|
||||
| | Outlook/Trajectory Sensors | Average Sensors |
|
||||
|--|---------------------------|-----------------|
|
||||
| **Purpose** | Dashboard display, quick visual overview | Automations, precise numeric comparisons |
|
||||
| **Output** | Classification (falling/stable/rising) | Exact price values (ct/kWh) |
|
||||
| **Best for** | "Should I worry about prices?" | "Is the future average below 15 ct?" |
|
||||
| **Use in** | Dashboard icons, status displays | Template conditions, numeric thresholds |
|
||||
|
||||
**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.
|
||||
100
docs/user/docs/sensors-volatility.md
Normal file
100
docs/user/docs/sensors-volatility.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Volatility 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. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
|
||||
|
||||
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 / Arithmetic 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 |
|
||||
|---|---|---|
|
||||
| <EntityRef id="today_volatility">Today's Price Volatility</EntityRef> | Volatility for the current calendar day | 00:00 - 23:59 today |
|
||||
| <EntityRef id="tomorrow_volatility">Tomorrow's Price Volatility</EntityRef> | Volatility for the next calendar day | 00:00 - 23:59 tomorrow |
|
||||
| **Next 24h Price Volatility** (`next_24h_volatility`) | Volatility for the next 24 hours from now | Rolling 24h forward |
|
||||
| <EntityRef id="today_tomorrow_volatility">Today + Tomorrow Price Volatility</EntityRef> | 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_volatility` | Volatility level (language-independent, always English) | `"moderate"` |
|
||||
| `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.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -135,10 +135,11 @@ const config: Config = {
|
|||
docs: {
|
||||
sidebar: {
|
||||
hideable: true,
|
||||
autoCollapseCategories: true,
|
||||
autoCollapseCategories: false,
|
||||
},
|
||||
},
|
||||
navbar: {
|
||||
hideOnScroll: true,
|
||||
title: 'Tibber Prices HA',
|
||||
logo: {
|
||||
alt: 'Tibber Prices Integration Logo',
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const sidebars: SidebarsConfig = {
|
|||
{
|
||||
type: 'category',
|
||||
label: '🚀 Getting Started',
|
||||
link: { type: 'doc', id: 'installation' },
|
||||
items: ['installation', 'configuration'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
|
|
@ -25,41 +26,71 @@ const sidebars: SidebarsConfig = {
|
|||
{
|
||||
type: 'category',
|
||||
label: '📖 Core Concepts',
|
||||
link: { type: 'doc', id: 'concepts' },
|
||||
items: ['concepts', 'glossary'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: '📊 Features',
|
||||
items: ['sensors', 'period-calculation', 'dynamic-icons', 'icon-colors', 'actions'],
|
||||
label: '📊 Sensors',
|
||||
link: { type: 'doc', id: 'sensors-overview' },
|
||||
items: [
|
||||
'sensors-overview',
|
||||
'sensors-average',
|
||||
'sensors-ratings-levels',
|
||||
'sensors-volatility',
|
||||
'sensors-trends',
|
||||
'sensors-timing',
|
||||
'sensors-energy-tax',
|
||||
],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: '🎨 Visualization',
|
||||
items: ['dashboard-examples', 'chart-examples'],
|
||||
label: '⏰ Price Periods',
|
||||
link: { type: 'doc', id: 'period-calculation' },
|
||||
items: ['period-calculation', 'period-relaxation'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: '🤖 Automation',
|
||||
label: '🎨 Dashboards & Charts',
|
||||
link: { type: 'doc', id: 'dashboard-examples' },
|
||||
items: ['dynamic-icons', 'icon-colors', 'dashboard-examples', 'chart-examples'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: '🤖 Automations',
|
||||
link: { type: 'doc', id: 'automation-examples' },
|
||||
items: ['automation-examples'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: '<27> Community',
|
||||
label: '📖 Reference',
|
||||
link: { type: 'doc', id: 'sensor-reference' },
|
||||
items: ['sensor-reference', 'actions'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: '👥 Community',
|
||||
link: { type: 'doc', id: 'community-examples' },
|
||||
items: ['community-examples'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: '<27>🔧 Help & Support',
|
||||
label: '🔧 Help & Support',
|
||||
link: { type: 'doc', id: 'faq' },
|
||||
items: ['faq', 'troubleshooting'],
|
||||
collapsible: true,
|
||||
collapsed: false,
|
||||
|
|
|
|||
48
docs/user/src/components/EntityRef.tsx
Normal file
48
docs/user/src/components/EntityRef.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
|
||||
interface EntityRefProps {
|
||||
/** Primary translation_key / entity ID suffix */
|
||||
id: string;
|
||||
/** Optional second key (for paired sensors like best/peak) */
|
||||
also?: string;
|
||||
/** Render without <strong> wrapper (default: false → bold) */
|
||||
noStrong?: boolean;
|
||||
/** Display name shown to the user */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact inline reference to an entity, linking to the multi-language
|
||||
* sensor reference table.
|
||||
*
|
||||
* Uses a relative URL so links stay within the current docs version
|
||||
* (e.g. /next/, /v0.30.0/, or the latest version).
|
||||
*
|
||||
* Usage:
|
||||
* <EntityRef id="average_price_today">Average Price Today</EntityRef>
|
||||
* <EntityRef id="best_price_end_time" also="peak_price_end_time">End Time</EntityRef>
|
||||
*/
|
||||
export default function EntityRef({
|
||||
id,
|
||||
also,
|
||||
noStrong,
|
||||
children,
|
||||
}: EntityRefProps): React.ReactElement {
|
||||
// Relative URL — browser resolves it relative to the current page,
|
||||
// which automatically preserves the versioned docs path prefix.
|
||||
const refUrl = `sensor-reference#ref-${id}`;
|
||||
const keys = also ? `${id} / ${also}` : id;
|
||||
const tooltip = `${keys} — View in all languages`;
|
||||
const content = noStrong ? children : <strong>{children}</strong>;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={refUrl}
|
||||
className="entity-ref"
|
||||
title={tooltip}
|
||||
aria-label={`Entity reference: ${keys}`}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
560
docs/user/src/components/EntitySearch.tsx
Normal file
560
docs/user/src/components/EntitySearch.tsx
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
interface RowEntry {
|
||||
/** The entity-anchor id (e.g. "ref-current_interval_price") */
|
||||
anchorId: string;
|
||||
/** The translation_key / entity ID suffix */
|
||||
key: string;
|
||||
/** English name (first translated name) */
|
||||
englishName: string;
|
||||
/** All translated names (for display in results) */
|
||||
translatedNames: string[];
|
||||
/** All searchable text from the row (names in all languages, key) */
|
||||
searchText: string;
|
||||
/** The <tr> element */
|
||||
row: HTMLTableRowElement;
|
||||
/** Platform heading (e.g. "Sensors", "Binary Sensors") */
|
||||
platform: string;
|
||||
/** Doc page slugs that reference this entity (from data-refs attribute) */
|
||||
docRefs: string[];
|
||||
/** Name cells (columns between key and default) for match highlighting */
|
||||
nameCells: HTMLTableCellElement[];
|
||||
/** Original innerHTML of name cells (for restoring after highlighting) */
|
||||
originalNameHTML: string[];
|
||||
}
|
||||
|
||||
const MAX_RESULTS = 12;
|
||||
|
||||
/** Display names for doc page back-links (from data-refs attribute). */
|
||||
const DOC_NAMES: Record<string, string> = {
|
||||
'sensors-overview': 'Sensors Overview',
|
||||
'sensors-average': 'Average Sensors',
|
||||
'sensors-ratings-levels': 'Ratings & Levels',
|
||||
'sensors-volatility': 'Volatility',
|
||||
'sensors-trends': 'Trends',
|
||||
'sensors-timing': 'Timing',
|
||||
'sensors-energy-tax': 'Energy & Tax',
|
||||
configuration: 'Configuration',
|
||||
'period-calculation': 'Period Calculation',
|
||||
'period-relaxation': 'Relaxation',
|
||||
'automation-examples': 'Automation Examples',
|
||||
actions: 'Actions',
|
||||
};
|
||||
|
||||
/** Platform filter chips. `match` is tested with startsWith against h2 text. */
|
||||
const PLATFORM_CHIPS = [
|
||||
{label: 'Sensors', match: 'Sensors'},
|
||||
{label: 'Binary Sensors', match: 'Binary Sensors'},
|
||||
{label: 'Numbers', match: 'Number Entities'},
|
||||
{label: 'Switches', match: 'Switch Entities'},
|
||||
];
|
||||
|
||||
/**
|
||||
* Highlight `needle` inside `html` by wrapping matches in <mark> tags.
|
||||
* Only replaces inside text nodes (outside HTML tags) to keep markup intact.
|
||||
*/
|
||||
function highlightHTML(html: string, needle: string): string {
|
||||
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const re = new RegExp(`(${escaped})`, 'gi');
|
||||
return html.replace(/(<[^>]*>)|([^<]+)/g, (_m, tag: string, text: string) => {
|
||||
if (tag) return tag;
|
||||
return text.replace(re, '<mark class="entity-match">$1</mark>');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Live-filtering search bar for the sensor-reference page.
|
||||
*
|
||||
* Scans all `.entity-anchor` spans on mount to build an index of
|
||||
* entity keys and translated names. Typing filters the tables in
|
||||
* real-time and shows a clickable result list to jump to entries.
|
||||
*/
|
||||
export default function EntitySearch(): React.ReactElement {
|
||||
const [query, setQuery] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
const [matchCount, setMatchCount] = useState(0);
|
||||
const [matches, setMatches] = useState<RowEntry[]>([]);
|
||||
const [activeIndex, setActiveIndex] = useState(-1);
|
||||
const [activeChip, setActiveChip] = useState<string | null>(null);
|
||||
const entriesRef = useRef<RowEntry[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const resultsRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
// ── Build the search index on mount ──────────────────────────
|
||||
useEffect(() => {
|
||||
const anchors = document.querySelectorAll<HTMLSpanElement>('.entity-anchor');
|
||||
const entries: RowEntry[] = [];
|
||||
|
||||
anchors.forEach((anchor) => {
|
||||
const row = anchor.closest('tr');
|
||||
if (!row) return;
|
||||
|
||||
const anchorId = anchor.id;
|
||||
const key = anchorId.replace(/^ref-/, '');
|
||||
|
||||
// Determine platform from closest h2 above this table
|
||||
let platform = '';
|
||||
const table = row.closest('table');
|
||||
if (table) {
|
||||
let el = table.previousElementSibling;
|
||||
while (el) {
|
||||
if (el.tagName === 'H2') {
|
||||
platform = el.textContent?.trim() ?? '';
|
||||
break;
|
||||
}
|
||||
el = el.previousElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
// Doc back-links from data attribute (set by generator)
|
||||
const refsAttr = anchor.getAttribute('data-refs');
|
||||
const docRefs = refsAttr ? refsAttr.split(',').filter(Boolean) : [];
|
||||
|
||||
// Collect text from all cells + store name cells for highlighting
|
||||
const cells = row.querySelectorAll('td');
|
||||
const texts: string[] = [key];
|
||||
const translatedNames: string[] = [];
|
||||
const nameCells: HTMLTableCellElement[] = [];
|
||||
const originalNameHTML: string[] = [];
|
||||
|
||||
cells.forEach((cell, i) => {
|
||||
const text = cell.textContent?.trim();
|
||||
if (text && text !== '✅' && text !== '❌') {
|
||||
texts.push(text);
|
||||
}
|
||||
// Name cells = columns between key (0) and default (last)
|
||||
if (i > 0 && i < cells.length - 1) {
|
||||
nameCells.push(cell);
|
||||
originalNameHTML.push(cell.innerHTML);
|
||||
const t = cell.textContent?.trim();
|
||||
if (t) translatedNames.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Inject copy-entity-ID button ──
|
||||
const firstCell = cells[0];
|
||||
if (firstCell && !firstCell.querySelector('.entity-copy-btn')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'entity-copy-btn';
|
||||
btn.title = 'Copy entity ID suffix';
|
||||
btn.setAttribute('aria-label', `Copy ${key}`);
|
||||
btn.textContent = '\u29C9'; // ⧉ overlapping squares
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
btn.textContent = '\u2713'; // ✓
|
||||
btn.classList.add('copied');
|
||||
setTimeout(() => {
|
||||
btn.textContent = '\u29C9';
|
||||
btn.classList.remove('copied');
|
||||
}, 1500);
|
||||
});
|
||||
});
|
||||
firstCell.appendChild(btn);
|
||||
}
|
||||
|
||||
// ── Inject doc back-links ──
|
||||
if (docRefs.length > 0 && firstCell && !firstCell.querySelector('.entity-back-links')) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'entity-back-links';
|
||||
docRefs.forEach((ref) => {
|
||||
const slug = ref.split('#')[0];
|
||||
const a = document.createElement('a');
|
||||
a.href = ref;
|
||||
a.className = 'entity-back-link';
|
||||
a.title = DOC_NAMES[slug] ?? slug;
|
||||
a.setAttribute('aria-label', `View in: ${DOC_NAMES[slug] ?? slug}`);
|
||||
a.textContent = '\uD83D\uDCD6'; // 📖
|
||||
span.appendChild(a);
|
||||
});
|
||||
firstCell.appendChild(span);
|
||||
}
|
||||
|
||||
entries.push({
|
||||
anchorId,
|
||||
key,
|
||||
englishName: translatedNames[0] ?? key,
|
||||
translatedNames,
|
||||
searchText: texts.join(' ').toLowerCase(),
|
||||
row,
|
||||
platform,
|
||||
docRefs,
|
||||
nameCells,
|
||||
originalNameHTML,
|
||||
});
|
||||
});
|
||||
|
||||
entriesRef.current = entries;
|
||||
setTotal(entries.length);
|
||||
setMatchCount(entries.length);
|
||||
}, []);
|
||||
|
||||
// ── Jump to #ref-* hash on arrival / hash change ─────────────
|
||||
useEffect(() => {
|
||||
const entries = entriesRef.current;
|
||||
if (entries.length === 0) return;
|
||||
|
||||
const jumpToHash = () => {
|
||||
const hash = window.location.hash;
|
||||
if (!hash.startsWith('#ref-')) return;
|
||||
|
||||
const key = hash.slice(5);
|
||||
const entry = entries.find((e) => e.key === key);
|
||||
if (!entry) return;
|
||||
|
||||
document.querySelectorAll('.entity-search-jump-highlight').forEach((el) => {
|
||||
el.classList.remove('entity-search-jump-highlight');
|
||||
});
|
||||
const anchor = document.getElementById(entry.anchorId);
|
||||
if (anchor) {
|
||||
requestAnimationFrame(() => {
|
||||
anchor.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
void entry.row.offsetWidth;
|
||||
entry.row.classList.add('entity-search-jump-highlight');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
jumpToHash();
|
||||
window.addEventListener('hashchange', jumpToHash);
|
||||
return () => window.removeEventListener('hashchange', jumpToHash);
|
||||
}, [total]);
|
||||
|
||||
// ── Global "/" shortcut to focus the search input ────────────
|
||||
useEffect(() => {
|
||||
const handleSlash = (e: KeyboardEvent) => {
|
||||
if (
|
||||
e.key === '/' &&
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!(e.target instanceof HTMLInputElement) &&
|
||||
!(e.target instanceof HTMLTextAreaElement) &&
|
||||
!(e.target instanceof HTMLSelectElement)
|
||||
) {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.scrollIntoView({behavior: 'smooth', block: 'nearest'});
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleSlash);
|
||||
return () => document.removeEventListener('keydown', handleSlash);
|
||||
}, []);
|
||||
|
||||
// ── Core filtering logic ─────────────────────────────────────
|
||||
const applyFilter = useCallback((search: string, chip: string | null) => {
|
||||
const entries = entriesRef.current;
|
||||
const needle = search.toLowerCase().trim();
|
||||
let matchCountLocal = 0;
|
||||
const matchedEntries: RowEntry[] = [];
|
||||
const sectionsWithMatches = new Set<Element>();
|
||||
|
||||
// Restore previous match highlights
|
||||
entries.forEach((entry) => {
|
||||
entry.nameCells.forEach((cell, i) => {
|
||||
if (cell.innerHTML !== entry.originalNameHTML[i]) {
|
||||
cell.innerHTML = entry.originalNameHTML[i];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
entries.forEach((entry) => {
|
||||
// Platform chip filter
|
||||
const chipMatch = !chip || entry.platform.startsWith(chip);
|
||||
// Text search filter
|
||||
const textMatch = !needle || entry.searchText.includes(needle);
|
||||
const isMatch = chipMatch && textMatch;
|
||||
|
||||
if (isMatch) {
|
||||
matchCountLocal++;
|
||||
matchedEntries.push(entry);
|
||||
entry.row.classList.remove('entity-search-hidden');
|
||||
entry.row.classList.add('entity-search-match');
|
||||
|
||||
// Highlight matched text in name cells
|
||||
if (needle) {
|
||||
entry.nameCells.forEach((cell, i) => {
|
||||
cell.innerHTML = highlightHTML(entry.originalNameHTML[i], needle);
|
||||
});
|
||||
}
|
||||
|
||||
const table = entry.row.closest('table');
|
||||
if (table) {
|
||||
let prev = table.previousElementSibling;
|
||||
while (prev && prev.tagName !== 'H3' && prev.tagName !== 'H2') {
|
||||
prev = prev.previousElementSibling;
|
||||
}
|
||||
if (prev) sectionsWithMatches.add(prev);
|
||||
}
|
||||
} else {
|
||||
entry.row.classList.add('entity-search-hidden');
|
||||
entry.row.classList.remove('entity-search-match');
|
||||
}
|
||||
});
|
||||
|
||||
const isActive = !!(needle || chip);
|
||||
if (isActive) {
|
||||
document.querySelectorAll('.markdown h3, .markdown h2').forEach((heading) => {
|
||||
if (heading.textContent?.includes('How to Find')) return;
|
||||
const hasMatch = sectionsWithMatches.has(heading);
|
||||
|
||||
if (heading.tagName === 'H3') {
|
||||
heading.classList.toggle('entity-search-section-hidden', !hasMatch);
|
||||
let el = heading.nextElementSibling;
|
||||
while (el && el.tagName !== 'TABLE' && el.tagName !== 'H3' && el.tagName !== 'H2') {
|
||||
el.classList.toggle('entity-search-section-hidden', !hasMatch);
|
||||
el = el.nextElementSibling;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll('.entity-search-section-hidden').forEach((el) => {
|
||||
el.classList.remove('entity-search-section-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
setMatchCount(matchCountLocal);
|
||||
setMatches(isActive ? matchedEntries : []);
|
||||
setActiveIndex(-1);
|
||||
}, []);
|
||||
|
||||
const scrollToEntry = useCallback((entry: RowEntry) => {
|
||||
document.querySelectorAll('.entity-search-jump-highlight').forEach((el) => {
|
||||
el.classList.remove('entity-search-jump-highlight');
|
||||
});
|
||||
|
||||
const anchor = document.getElementById(entry.anchorId);
|
||||
if (anchor) {
|
||||
history.pushState(null, '', `#${entry.anchorId}`);
|
||||
anchor.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
void entry.row.offsetWidth;
|
||||
entry.row.classList.add('entity-search-jump-highlight');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setQuery(value);
|
||||
applyFilter(value, activeChip);
|
||||
},
|
||||
[applyFilter, activeChip],
|
||||
);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setQuery('');
|
||||
setActiveChip(null);
|
||||
applyFilter('', null);
|
||||
inputRef.current?.focus();
|
||||
}, [applyFilter]);
|
||||
|
||||
// Click anywhere outside the search bar → reset all filters
|
||||
useEffect(() => {
|
||||
if (query.trim().length === 0 && activeChip === null) return;
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
// Don't reset if clicking inside the search container
|
||||
if (containerRef.current?.contains(target)) return;
|
||||
// Don't reset if clicking inside an entity table or its buttons
|
||||
if ((target as Element).closest?.('.entity-copy-btn, .entity-back-link, table')) return;
|
||||
|
||||
setQuery('');
|
||||
setActiveChip(null);
|
||||
applyFilter('', null);
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}, [query, activeChip, applyFilter]);
|
||||
|
||||
const handleChipClick = useCallback(
|
||||
(chipMatch: string) => {
|
||||
const next = chipMatch === activeChip ? null : chipMatch;
|
||||
setActiveChip(next);
|
||||
applyFilter(query, next);
|
||||
},
|
||||
[applyFilter, query, activeChip],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClear();
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleMatches = matches.slice(0, MAX_RESULTS);
|
||||
if (visibleMatches.length === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setActiveIndex((prev) => {
|
||||
const next = prev < visibleMatches.length - 1 ? prev + 1 : 0;
|
||||
resultsRef.current?.children[next]?.scrollIntoView({block: 'nearest'});
|
||||
return next;
|
||||
});
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setActiveIndex((prev) => {
|
||||
const next = prev > 0 ? prev - 1 : visibleMatches.length - 1;
|
||||
resultsRef.current?.children[next]?.scrollIntoView({block: 'nearest'});
|
||||
return next;
|
||||
});
|
||||
} else if (e.key === 'Enter' && activeIndex >= 0 && activeIndex < visibleMatches.length) {
|
||||
e.preventDefault();
|
||||
scrollToEntry(visibleMatches[activeIndex]);
|
||||
}
|
||||
},
|
||||
[handleClear, matches, activeIndex, scrollToEntry],
|
||||
);
|
||||
|
||||
/** Find the best matching translated name for highlighting in dropdown */
|
||||
const getMatchingName = useCallback(
|
||||
(entry: RowEntry, needle: string): string | null => {
|
||||
if (!needle) return null;
|
||||
const lower = needle.toLowerCase();
|
||||
// Check non-English names first (user is likely searching in their language)
|
||||
for (let i = 1; i < entry.translatedNames.length; i++) {
|
||||
if (entry.translatedNames[i].toLowerCase().includes(lower)) {
|
||||
return entry.translatedNames[i];
|
||||
}
|
||||
}
|
||||
// Then English
|
||||
if (entry.englishName.toLowerCase().includes(lower)) {
|
||||
return null; // Already shown as primary
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const isFiltering = query.trim().length > 0 || activeChip !== null;
|
||||
const visibleMatches = matches.slice(0, MAX_RESULTS);
|
||||
const hasMore = matches.length > MAX_RESULTS;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="entity-search">
|
||||
{/* ── Category filter chips ── */}
|
||||
<div className="entity-search-chips" role="group" aria-label="Filter by platform">
|
||||
{PLATFORM_CHIPS.map((chip) => (
|
||||
<button
|
||||
key={chip.match}
|
||||
type="button"
|
||||
className={`entity-search-chip${activeChip === chip.match ? ' active' : ''}`}
|
||||
onClick={() => handleChipClick(chip.match)}
|
||||
aria-pressed={activeChip === chip.match}
|
||||
>
|
||||
{chip.label}
|
||||
</button>
|
||||
))}
|
||||
<span className="entity-search-shortcut-hint">
|
||||
Press <kbd>/</kbd> to search
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ── Search input ── */}
|
||||
<div className="entity-search-input-wrapper">
|
||||
<svg
|
||||
className="entity-search-icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
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" />
|
||||
</svg>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="entity-search-input"
|
||||
placeholder="Search entities (any language)…"
|
||||
value={query}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label="Search entities"
|
||||
aria-expanded={isFiltering && visibleMatches.length > 0}
|
||||
aria-controls="entity-search-results"
|
||||
aria-activedescendant={
|
||||
activeIndex >= 0 ? `entity-result-${activeIndex}` : undefined
|
||||
}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
role="combobox"
|
||||
/>
|
||||
{isFiltering && (
|
||||
<button
|
||||
className="entity-search-clear"
|
||||
onClick={handleClear}
|
||||
aria-label="Clear search and filters"
|
||||
type="button"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Results dropdown ── */}
|
||||
{isFiltering && (
|
||||
<div className="entity-search-results-container">
|
||||
{matchCount === 0 ? (
|
||||
<div className="entity-search-status">
|
||||
<span className="entity-search-no-results">No matching entities found</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{query.trim().length > 0 && (
|
||||
<ul
|
||||
ref={resultsRef}
|
||||
id="entity-search-results"
|
||||
className="entity-search-results"
|
||||
role="listbox"
|
||||
>
|
||||
{visibleMatches.map((entry, i) => {
|
||||
const matchedTranslation = getMatchingName(entry, query);
|
||||
return (
|
||||
<li
|
||||
key={entry.key}
|
||||
id={`entity-result-${i}`}
|
||||
className={`entity-search-result-item${i === activeIndex ? ' active' : ''}`}
|
||||
role="option"
|
||||
aria-selected={i === activeIndex}
|
||||
onClick={() => scrollToEntry(entry)}
|
||||
onMouseEnter={() => setActiveIndex(i)}
|
||||
>
|
||||
<span className="entity-search-result-name">
|
||||
{entry.englishName}
|
||||
</span>
|
||||
<code className="entity-search-result-key">{entry.key}</code>
|
||||
{matchedTranslation && (
|
||||
<span className="entity-search-result-translation">
|
||||
{matchedTranslation}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
<div className="entity-search-status">
|
||||
{matchCount} of {total} entities
|
||||
{hasMore && query.trim().length > 0 && (
|
||||
<span className="entity-search-more">
|
||||
{' '}— showing first {MAX_RESULTS}, type more to narrow down
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,6 +4,440 @@
|
|||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* ── EntityRef component ──────────────────────────────────────── */
|
||||
|
||||
/* The link wrapping the entity name */
|
||||
.entity-ref {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-bottom: 1.5px dotted var(--ifm-color-primary);
|
||||
transition: border-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.entity-ref:hover {
|
||||
border-bottom-style: solid;
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Small arrow indicator after the name */
|
||||
.entity-ref::after {
|
||||
content: '\2197'; /* ↗ */
|
||||
display: inline-block;
|
||||
font-size: 0.6em;
|
||||
vertical-align: super;
|
||||
margin-left: 0.15em;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.entity-ref:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Inside tables, keep the indicator subtle */
|
||||
td .entity-ref::after {
|
||||
font-size: 0.55em;
|
||||
}
|
||||
|
||||
/* ── EntitySearch component ────────────────────────────────────── */
|
||||
|
||||
.entity-search {
|
||||
position: sticky;
|
||||
top: calc(var(--ifm-navbar-height) + 0.5rem);
|
||||
z-index: 10;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--ifm-background-surface-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: var(--ifm-global-shadow-md);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .entity-search {
|
||||
background: rgba(36, 36, 36, 0.92);
|
||||
border-color: var(--ifm-color-emphasis-300);
|
||||
}
|
||||
|
||||
.entity-search-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entity-search-icon {
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
color: var(--ifm-color-emphasis-500);
|
||||
pointer-events: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.entity-search-input {
|
||||
width: 100%;
|
||||
padding: 0.6rem 2.5rem 0.6rem 2.5rem;
|
||||
border: 1.5px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: calc(var(--ifm-global-radius) - 2px);
|
||||
background: var(--ifm-background-color);
|
||||
color: var(--ifm-font-color-base);
|
||||
font-size: 0.95rem;
|
||||
font-family: var(--ifm-font-family-base);
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.entity-search-input::placeholder {
|
||||
color: var(--ifm-color-emphasis-500);
|
||||
}
|
||||
|
||||
.entity-search-input:focus {
|
||||
border-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(0, 185, 231, 0.15);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .entity-search-input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.15);
|
||||
}
|
||||
|
||||
.entity-search-clear {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--ifm-color-emphasis-200);
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.entity-search-clear:hover {
|
||||
background: var(--ifm-color-emphasis-300);
|
||||
color: var(--ifm-color-emphasis-900);
|
||||
}
|
||||
|
||||
.entity-search-status {
|
||||
margin-top: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--ifm-color-emphasis-600);
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.entity-search-no-results {
|
||||
color: var(--ifm-color-danger);
|
||||
}
|
||||
|
||||
/* ── Results dropdown ─────────────────────────────────────────── */
|
||||
|
||||
.entity-search-results-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.entity-search-results {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0.25rem 0;
|
||||
max-height: 24rem;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: calc(var(--ifm-global-radius) - 2px);
|
||||
background: var(--ifm-background-color);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .entity-search-results {
|
||||
border-color: var(--ifm-color-emphasis-300);
|
||||
}
|
||||
|
||||
.entity-search-result-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.entity-search-result-item:hover,
|
||||
.entity-search-result-item.active {
|
||||
background: var(--ifm-color-primary-lightest);
|
||||
color: var(--ifm-font-color-base);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .entity-search-result-item:hover,
|
||||
[data-theme='dark'] .entity-search-result-item.active {
|
||||
background: rgba(0, 212, 255, 0.12);
|
||||
}
|
||||
|
||||
.entity-search-result-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entity-search-result-key {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entity-search-result-translation {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ifm-color-emphasis-600);
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entity-search-more {
|
||||
color: var(--ifm-color-emphasis-500);
|
||||
}
|
||||
|
||||
/* ── Jump highlight (triggered by clicking a result) ──────────── */
|
||||
|
||||
tr.entity-search-jump-highlight {
|
||||
animation: entity-jump-flash 1.8s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes entity-jump-flash {
|
||||
0% { background-color: rgba(0, 185, 231, 0.45); }
|
||||
30% { background-color: rgba(0, 185, 231, 0.25); }
|
||||
100% { background-color: rgba(0, 185, 231, 0.13); }
|
||||
}
|
||||
|
||||
[data-theme='dark'] tr.entity-search-jump-highlight {
|
||||
animation: entity-jump-flash-dark 1.8s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes entity-jump-flash-dark {
|
||||
0% { background-color: rgba(0, 212, 255, 0.40); }
|
||||
30% { background-color: rgba(0, 212, 255, 0.20); }
|
||||
100% { background-color: rgba(0, 212, 255, 0.13); }
|
||||
}
|
||||
|
||||
/* Row filtering states */
|
||||
tr.entity-search-hidden {
|
||||
opacity: 0.08;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
tr.entity-search-match {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
/* Dim sections with no matches */
|
||||
.entity-search-section-hidden {
|
||||
opacity: 0.15;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
/* ── Filter chips ─────────────────────────────────────────────── */
|
||||
|
||||
.entity-search-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.6rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entity-search-chip {
|
||||
padding: 0.25rem 0.7rem;
|
||||
border: 1.5px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: 2rem;
|
||||
background: transparent;
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--ifm-font-family-base);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entity-search-chip:hover {
|
||||
border-color: var(--ifm-color-primary);
|
||||
color: var(--ifm-color-primary);
|
||||
background: rgba(0, 185, 231, 0.06);
|
||||
}
|
||||
|
||||
.entity-search-chip.active {
|
||||
border-color: var(--ifm-color-primary);
|
||||
background: var(--ifm-color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .entity-search-chip.active {
|
||||
background: var(--ifm-color-primary-dark);
|
||||
}
|
||||
|
||||
.entity-search-shortcut-hint {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
color: var(--ifm-color-emphasis-400);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entity-search-shortcut-hint kbd {
|
||||
display: inline-block;
|
||||
padding: 0.05rem 0.35rem;
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: 0.25rem;
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
font-family: var(--ifm-font-family-monospace);
|
||||
font-size: 0.75rem;
|
||||
color: var(--ifm-color-emphasis-600);
|
||||
line-height: 1.4;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* ── Copy entity ID button ────────────────────────────────────── */
|
||||
|
||||
.entity-copy-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
margin-left: 0.35rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
background: transparent;
|
||||
color: var(--ifm-color-emphasis-400);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr:hover .entity-copy-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Always visible on touch devices (no hover) */
|
||||
@media (hover: none) {
|
||||
.entity-copy-btn {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-copy-btn:hover {
|
||||
background: var(--ifm-color-emphasis-200);
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
}
|
||||
|
||||
.entity-copy-btn.copied {
|
||||
color: var(--ifm-color-success);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── Doc back-links ───────────────────────────────────────────── */
|
||||
|
||||
.entity-back-links {
|
||||
margin-left: 0.25rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entity-back-link {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
text-decoration: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
margin-left: 0.1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr:hover .entity-back-link {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.entity-back-link:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.entity-back-link {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Match highlighting ──────────────────────────────────────── */
|
||||
|
||||
mark.entity-match {
|
||||
background-color: rgba(255, 184, 0, 0.35);
|
||||
color: inherit;
|
||||
padding: 0.05em 0.1em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
[data-theme='dark'] mark.entity-match {
|
||||
background-color: rgba(255, 184, 0, 0.3);
|
||||
}
|
||||
|
||||
/* ── Mobile responsive ────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.entity-search {
|
||||
position: relative;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.entity-search-chips {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.entity-search-shortcut-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.entity-search-results {
|
||||
max-height: 16rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Sensor-reference :target highlighting ────────────────────── */
|
||||
|
||||
/* Scroll offset so the targeted row isn't hidden behind the navbar */
|
||||
.entity-anchor {
|
||||
scroll-margin-top: 5rem;
|
||||
}
|
||||
|
||||
/* Highlight the table row that was navigated to (stays visible) */
|
||||
tr:has(.entity-anchor:target) {
|
||||
animation: entity-highlight-pulse 2s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes entity-highlight-pulse {
|
||||
0% { background-color: rgba(0, 185, 231, 0.35); }
|
||||
100% { background-color: rgba(0, 185, 231, 0.13); }
|
||||
}
|
||||
|
||||
[data-theme='dark'] tr:has(.entity-anchor:target) {
|
||||
animation: entity-highlight-pulse-dark 2s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes entity-highlight-pulse-dark {
|
||||
0% { background-color: rgba(0, 212, 255, 0.30); }
|
||||
100% { background-color: rgba(0, 212, 255, 0.13); }
|
||||
}
|
||||
|
||||
/* Smooth scrolling for anchor navigation */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Modern font stack */
|
||||
:root {
|
||||
--ifm-font-family-base: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
|
|
|
|||
13
docs/user/src/theme/MDXComponents.tsx
Normal file
13
docs/user/src/theme/MDXComponents.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Extend the default MDXComponents so that <EntityRef> and <EntitySearch>
|
||||
* are available in every .mdx / .md page without explicit imports.
|
||||
*/
|
||||
import MDXComponents from '@theme-original/MDXComponents';
|
||||
import EntityRef from '@site/src/components/EntityRef';
|
||||
import EntitySearch from '@site/src/components/EntitySearch';
|
||||
|
||||
export default {
|
||||
...MDXComponents,
|
||||
EntityRef,
|
||||
EntitySearch,
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "Tibber Price Information & Ratings",
|
||||
"homeassistant": "2025.10.0",
|
||||
"homeassistant": "2026.4.0",
|
||||
"hacs": "2.0.5"
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ typeCheckingMode = "basic"
|
|||
|
||||
[tool.ruff]
|
||||
# Based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
|
||||
target-version = "py313"
|
||||
target-version = "py314"
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
|
@ -39,6 +39,7 @@ ignore = [
|
|||
"D212", # multi-line-summary-first-line (incompatible with formatter)
|
||||
"COM812", # incompatible with formatter
|
||||
"ISC001", # incompatible with formatter
|
||||
"UP037", # quoted annotations; needed for TYPE_CHECKING forward references
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
|
|
@ -46,6 +47,10 @@ ignore = [
|
|||
"S101", # assert is fine in tests
|
||||
"PLR2004", # Magic values are fine in tests
|
||||
]
|
||||
"scripts/*" = [
|
||||
"T201", # print() is the correct output method for CLI scripts
|
||||
"INP001", # scripts/ is not a Python package (no __init__.py)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
|
|
|
|||
|
|
@ -27,5 +27,9 @@ fi
|
|||
"$SCRIPT_DIR/type-check"
|
||||
echo ""
|
||||
"$SCRIPT_DIR/lint-check"
|
||||
echo ""
|
||||
|
||||
log_header "Checking sensor reference freshness..."
|
||||
python3 "$SCRIPT_DIR/docs/generate-sensor-reference" --check
|
||||
|
||||
log_success "All checks passed"
|
||||
|
|
|
|||
610
scripts/docs/generate-sensor-reference
Executable file
610
scripts/docs/generate-sensor-reference
Executable file
|
|
@ -0,0 +1,610 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate the multi-language sensor reference page from translation files.
|
||||
|
||||
Reads entity translations from all language files and entity definitions
|
||||
to produce a searchable reference table in docs/user/docs/sensor-reference.md.
|
||||
|
||||
Usage:
|
||||
scripts/docs/generate-sensor-reference # Generate/update the file
|
||||
scripts/docs/generate-sensor-reference --check # Verify file is up-to-date (CI)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
TRANSLATIONS_DIR = REPO_ROOT / "custom_components" / "tibber_prices" / "translations"
|
||||
OUTPUT_FILE = REPO_ROOT / "docs" / "user" / "docs" / "sensor-reference.md"
|
||||
|
||||
LANGUAGES = OrderedDict(
|
||||
[
|
||||
("en", "🇬🇧 English"),
|
||||
("de", "🇩🇪 Deutsch"),
|
||||
("nb", "🇳🇴 Norsk"),
|
||||
("nl", "🇳🇱 Nederlands"),
|
||||
("sv", "🇸🇪 Svenska"),
|
||||
]
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Definitions files (for entity_registry_enabled_default extraction)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFINITIONS_FILES: dict[str, Path] = {
|
||||
"sensor": REPO_ROOT / "custom_components" / "tibber_prices" / "sensor" / "definitions.py",
|
||||
"binary_sensor": REPO_ROOT / "custom_components" / "tibber_prices" / "binary_sensor" / "definitions.py",
|
||||
"number": REPO_ROOT / "custom_components" / "tibber_prices" / "number" / "definitions.py",
|
||||
"switch": REPO_ROOT / "custom_components" / "tibber_prices" / "switch" / "definitions.py",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Category mapping: translation_key → (category_name, sort_order)
|
||||
#
|
||||
# Keys not listed here will appear in an "Other" category at the end.
|
||||
# Order within a category follows insertion order in this dict.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SENSOR_CATEGORIES: OrderedDict[str, list[str]] = OrderedDict(
|
||||
[
|
||||
(
|
||||
"Core Price Sensors",
|
||||
[
|
||||
"current_interval_price",
|
||||
"current_interval_price_base",
|
||||
"next_interval_price",
|
||||
"previous_interval_price",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Hourly Average Sensors",
|
||||
[
|
||||
"current_hour_average_price",
|
||||
"next_hour_average_price",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Daily Statistics",
|
||||
[
|
||||
"lowest_price_today",
|
||||
"highest_price_today",
|
||||
"average_price_today",
|
||||
"lowest_price_tomorrow",
|
||||
"highest_price_tomorrow",
|
||||
"average_price_tomorrow",
|
||||
],
|
||||
),
|
||||
(
|
||||
"24h Window Sensors",
|
||||
[
|
||||
"trailing_price_average",
|
||||
"leading_price_average",
|
||||
"trailing_price_min",
|
||||
"trailing_price_max",
|
||||
"leading_price_min",
|
||||
"leading_price_max",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Future Price Averages",
|
||||
[
|
||||
"next_avg_1h",
|
||||
"next_avg_2h",
|
||||
"next_avg_3h",
|
||||
"next_avg_4h",
|
||||
"next_avg_5h",
|
||||
"next_avg_6h",
|
||||
"next_avg_8h",
|
||||
"next_avg_12h",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Price Level Sensors",
|
||||
[
|
||||
"current_interval_price_level",
|
||||
"next_interval_price_level",
|
||||
"previous_interval_price_level",
|
||||
"current_hour_price_level",
|
||||
"next_hour_price_level",
|
||||
"yesterday_price_level",
|
||||
"today_price_level",
|
||||
"tomorrow_price_level",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Price Rating Sensors",
|
||||
[
|
||||
"current_interval_price_rating",
|
||||
"next_interval_price_rating",
|
||||
"previous_interval_price_rating",
|
||||
"current_hour_price_rating",
|
||||
"next_hour_price_rating",
|
||||
"yesterday_price_rating",
|
||||
"today_price_rating",
|
||||
"tomorrow_price_rating",
|
||||
"daily_rating",
|
||||
"monthly_rating",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Price Outlook & Trend",
|
||||
[
|
||||
"current_price_trend",
|
||||
"next_price_trend_change",
|
||||
"next_price_trend_change_in",
|
||||
"price_outlook_1h",
|
||||
"price_outlook_2h",
|
||||
"price_outlook_3h",
|
||||
"price_outlook_4h",
|
||||
"price_outlook_5h",
|
||||
"price_outlook_6h",
|
||||
"price_outlook_8h",
|
||||
"price_outlook_12h",
|
||||
"price_trajectory_2h",
|
||||
"price_trajectory_3h",
|
||||
"price_trajectory_4h",
|
||||
"price_trajectory_5h",
|
||||
"price_trajectory_6h",
|
||||
"price_trajectory_8h",
|
||||
"price_trajectory_12h",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Volatility Sensors",
|
||||
[
|
||||
"today_volatility",
|
||||
"tomorrow_volatility",
|
||||
"next_24h_volatility",
|
||||
"today_tomorrow_volatility",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Best Price Timing",
|
||||
[
|
||||
"best_price_end_time",
|
||||
"best_price_period_duration",
|
||||
"best_price_remaining_minutes",
|
||||
"best_price_progress",
|
||||
"best_price_next_start_time",
|
||||
"best_price_next_in_minutes",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Peak Price Timing",
|
||||
[
|
||||
"peak_price_end_time",
|
||||
"peak_price_period_duration",
|
||||
"peak_price_remaining_minutes",
|
||||
"peak_price_progress",
|
||||
"peak_price_next_start_time",
|
||||
"peak_price_next_in_minutes",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Home & Metering Metadata",
|
||||
[
|
||||
"home_type",
|
||||
"home_size",
|
||||
"main_fuse_size",
|
||||
"number_of_residents",
|
||||
"primary_heating_source",
|
||||
"grid_company",
|
||||
"grid_area_code",
|
||||
"price_area_code",
|
||||
"consumption_ean",
|
||||
"production_ean",
|
||||
"energy_tax_type",
|
||||
"vat_type",
|
||||
"estimated_annual_consumption",
|
||||
"subscription_status",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Data & Diagnostics",
|
||||
[
|
||||
"data_lifecycle_status",
|
||||
"chart_data_export",
|
||||
"chart_metadata",
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
BINARY_SENSOR_CATEGORIES: OrderedDict[str, list[str]] = OrderedDict(
|
||||
[
|
||||
(
|
||||
"Binary Sensors",
|
||||
[
|
||||
"best_price_period",
|
||||
"peak_price_period",
|
||||
"connection",
|
||||
"tomorrow_data_available",
|
||||
"has_ventilation_system",
|
||||
"realtime_consumption_enabled",
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
NUMBER_CATEGORIES: OrderedDict[str, list[str]] = OrderedDict(
|
||||
[
|
||||
(
|
||||
"Best Price Configuration",
|
||||
[
|
||||
"best_price_flex_override",
|
||||
"best_price_min_distance_override",
|
||||
"best_price_min_period_length_override",
|
||||
"best_price_min_periods_override",
|
||||
"best_price_relaxation_attempts_override",
|
||||
"best_price_gap_count_override",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Peak Price Configuration",
|
||||
[
|
||||
"peak_price_flex_override",
|
||||
"peak_price_min_distance_override",
|
||||
"peak_price_min_period_length_override",
|
||||
"peak_price_min_periods_override",
|
||||
"peak_price_relaxation_attempts_override",
|
||||
"peak_price_gap_count_override",
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
SWITCH_CATEGORIES: OrderedDict[str, list[str]] = OrderedDict(
|
||||
[
|
||||
(
|
||||
"Switches",
|
||||
[
|
||||
"best_price_enable_relaxation_override",
|
||||
"peak_price_enable_relaxation_override",
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_translations() -> dict[str, dict[str, dict[str, dict]]]:
|
||||
"""
|
||||
Load entity translations from all language files.
|
||||
|
||||
Returns: {lang: {platform: {key: {"name": "..."}}}}
|
||||
"""
|
||||
result: dict[str, dict[str, dict[str, dict]]] = {}
|
||||
for lang in LANGUAGES:
|
||||
filepath = TRANSLATIONS_DIR / f"{lang}.json"
|
||||
with filepath.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
entity_section = data.get("entity", {})
|
||||
result[lang] = {}
|
||||
for platform in ("sensor", "binary_sensor", "number", "switch"):
|
||||
result[lang][platform] = entity_section.get(platform, {})
|
||||
return result
|
||||
|
||||
|
||||
def extract_disabled_entities(definitions_path: Path) -> set[str]:
|
||||
"""
|
||||
Extract entity keys that have entity_registry_enabled_default=False.
|
||||
|
||||
Uses regex parsing — no Python import needed.
|
||||
"""
|
||||
disabled: set[str] = set()
|
||||
if not definitions_path.exists():
|
||||
return disabled
|
||||
|
||||
text = definitions_path.read_text(encoding="utf-8")
|
||||
|
||||
# Find all key= assignments, then check if the block before the next
|
||||
# key= contains entity_registry_enabled_default=False.
|
||||
key_pattern = re.compile(r'key="([^"]+)"')
|
||||
disabled_pattern = re.compile(r"entity_registry_enabled_default\s*=\s*False")
|
||||
|
||||
keys_with_pos = [(m.group(1), m.start()) for m in key_pattern.finditer(text)]
|
||||
|
||||
for i, (key, start) in enumerate(keys_with_pos):
|
||||
# Get the text between this key and the next key (or end of file)
|
||||
end = keys_with_pos[i + 1][1] if i + 1 < len(keys_with_pos) else len(text)
|
||||
block = text[start:end]
|
||||
|
||||
if disabled_pattern.search(block):
|
||||
disabled.add(key)
|
||||
# If neither pattern found, default is True (enabled)
|
||||
|
||||
return disabled
|
||||
|
||||
|
||||
def load_all_disabled() -> dict[str, set[str]]:
|
||||
"""Load disabled-by-default entity keys for all platforms."""
|
||||
result: dict[str, set[str]] = {}
|
||||
for platform, path in DEFINITIONS_FILES.items():
|
||||
result[platform] = extract_disabled_entities(path)
|
||||
return result
|
||||
|
||||
|
||||
def _heading_to_anchor(heading_text: str) -> str:
|
||||
"""Convert a Markdown heading to a Docusaurus-style anchor slug."""
|
||||
text = heading_text.strip().lower()
|
||||
# Remove inline code backticks and common Markdown formatting
|
||||
text = re.sub(r"[`*_]", "", text)
|
||||
# Replace non-alphanumeric (except hyphens) with hyphens
|
||||
text = re.sub(r"[^a-z0-9-]+", "-", text)
|
||||
# Collapse multiple hyphens and strip leading/trailing hyphens
|
||||
return re.sub(r"-{2,}", "-", text).strip("-")
|
||||
|
||||
|
||||
def scan_doc_refs() -> dict[str, list[str]]:
|
||||
"""
|
||||
Scan doc markdown files for EntityRef usage.
|
||||
|
||||
Returns: {entity_key: [doc_slug#anchor, ...]}
|
||||
Each value includes the section anchor of the nearest heading above the EntityRef.
|
||||
"""
|
||||
refs: dict[str, list[str]] = {}
|
||||
docs_dir = REPO_ROOT / "docs" / "user" / "docs"
|
||||
entity_ref_pattern = re.compile(r'<EntityRef\s[^>]*?\bid="([^"]+)"')
|
||||
also_pattern = re.compile(r'\balso="([^"]+)"')
|
||||
heading_pattern = re.compile(r"^(#{2,6})\s+(.+)$", re.MULTILINE)
|
||||
|
||||
for md_file in sorted(docs_dir.glob("*.md")):
|
||||
if md_file.name == "sensor-reference.md":
|
||||
continue
|
||||
slug = md_file.stem
|
||||
text = md_file.read_text(encoding="utf-8")
|
||||
|
||||
# Build list of (position, anchor) for all headings in this file
|
||||
headings: list[tuple[int, str]] = [
|
||||
(h_match.start(), _heading_to_anchor(h_match.group(2))) for h_match in heading_pattern.finditer(text)
|
||||
]
|
||||
|
||||
def _ref_with_anchor(pos: int, *, _headings: list[tuple[int, str]] = headings, _slug: str = slug) -> str:
|
||||
"""Return 'slug#anchor' for the nearest heading above pos."""
|
||||
anchor = ""
|
||||
for h_pos, h_anchor in reversed(_headings):
|
||||
if h_pos < pos:
|
||||
anchor = h_anchor
|
||||
break
|
||||
return f"{_slug}#{anchor}" if anchor else _slug
|
||||
|
||||
def _add_ref(key: str, ref: str) -> None:
|
||||
refs.setdefault(key, [])
|
||||
if ref not in refs[key]:
|
||||
refs[key].append(ref)
|
||||
|
||||
for match in entity_ref_pattern.finditer(text):
|
||||
key = match.group(1)
|
||||
ref = _ref_with_anchor(match.start())
|
||||
_add_ref(key, ref)
|
||||
# Check for 'also' prop in the same tag
|
||||
tag_end = text.find(">", match.start())
|
||||
if tag_end != -1:
|
||||
tag_text = text[match.start() : tag_end]
|
||||
also_match = also_pattern.search(tag_text)
|
||||
if also_match:
|
||||
_add_ref(also_match.group(1), ref)
|
||||
|
||||
return refs
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markdown generation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FRONTMATTER = """\
|
||||
---
|
||||
comments: false
|
||||
---
|
||||
|
||||
"""
|
||||
|
||||
INTRO = """\
|
||||
# Entity Reference (All Languages)
|
||||
|
||||
<EntitySearch />
|
||||
|
||||
## How to Find Your Entity in Home Assistant
|
||||
|
||||
**Entity ID pattern:** `sensor.<home_name>_<suffix>`
|
||||
|
||||
- `<home_name>` is generated from your Tibber home display name (lowercase, spaces replaced with underscores)
|
||||
- `<suffix>` is shown in the **Entity ID suffix** column below
|
||||
|
||||
**Three ways to find an entity:**
|
||||
|
||||
1. **Search above** — Type the entity name in your language to filter the tables below
|
||||
2. **Device page** — Go to **Settings → Devices & Services → Tibber Prices** →
|
||||
click your home device → all entities are listed
|
||||
3. **Developer Tools** — Go to **Developer Tools → States** →
|
||||
type `tibber` in the filter
|
||||
|
||||
:::tip
|
||||
You can also use your browser's built-in search (**Ctrl+F** / **Cmd+F**) to search the full page text.
|
||||
:::
|
||||
|
||||
**Enabled by default:** The ✅ column shows whether a sensor is enabled by default.
|
||||
Sensors marked ❌ must be enabled manually via
|
||||
**Settings → Devices & Services → Entities** → find the entity → toggle **Enabled**.
|
||||
|
||||
**Detailed documentation:** See the **[Sensors Overview](sensors-overview.md)** for detailed
|
||||
explanations of each sensor's purpose, attributes, and automation examples.
|
||||
|
||||
---
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def generate_table(
|
||||
categories: OrderedDict[str, list[str]],
|
||||
platform: str,
|
||||
translations: dict[str, dict[str, dict[str, dict]]],
|
||||
disabled: dict[str, set[str]],
|
||||
doc_refs: dict[str, list[str]],
|
||||
) -> str:
|
||||
"""Generate a grouped Markdown table for one platform."""
|
||||
lines: list[str] = []
|
||||
platform_disabled = disabled.get(platform, set())
|
||||
|
||||
lang_codes = list(LANGUAGES.keys())
|
||||
lang_headers = list(LANGUAGES.values())
|
||||
|
||||
# Collect uncategorized keys
|
||||
all_categorized: set[str] = set()
|
||||
for keys in categories.values():
|
||||
all_categorized.update(keys)
|
||||
|
||||
# Get all keys from English translations for this platform
|
||||
en_keys = set(translations.get("en", {}).get(platform, {}).keys())
|
||||
uncategorized = en_keys - all_categorized
|
||||
|
||||
for category_name, keys in categories.items():
|
||||
lines.append(f"### {category_name}\n")
|
||||
lines.append("")
|
||||
|
||||
# Table header
|
||||
header = "| Entity ID suffix | " + " | ".join(lang_headers) + " | Default |"
|
||||
separator = "|---|" + "|".join(["---"] * len(lang_codes)) + "|---|"
|
||||
lines.append(header)
|
||||
lines.append(separator)
|
||||
|
||||
for key in keys:
|
||||
names: list[str] = []
|
||||
for lang in lang_codes:
|
||||
platform_trans = translations.get(lang, {}).get(platform, {})
|
||||
entity_data = platform_trans.get(key, {})
|
||||
name = entity_data.get("name", "—")
|
||||
names.append(name)
|
||||
|
||||
enabled = "❌" if key in platform_disabled else "✅"
|
||||
ref_list = doc_refs.get(key, [])
|
||||
data_refs_attr = f' data-refs="{",".join(ref_list)}"' if ref_list else ""
|
||||
anchor = f'<span id="ref-{key}" class="entity-anchor"{data_refs_attr}></span>'
|
||||
row = f"| {anchor}`{key}` | " + " | ".join(names) + f" | {enabled} |"
|
||||
lines.append(row)
|
||||
|
||||
lines.append("")
|
||||
|
||||
# Add uncategorized keys if any
|
||||
if uncategorized:
|
||||
lines.append("### Other\n")
|
||||
lines.append("")
|
||||
|
||||
header = "| Entity ID suffix | " + " | ".join(lang_headers) + " | Default |"
|
||||
separator = "|---|" + "|".join(["---"] * len(lang_codes)) + "|---|"
|
||||
lines.append(header)
|
||||
lines.append(separator)
|
||||
|
||||
for key in sorted(uncategorized):
|
||||
names = []
|
||||
for lang in lang_codes:
|
||||
platform_trans = translations.get(lang, {}).get(platform, {})
|
||||
entity_data = platform_trans.get(key, {})
|
||||
name = entity_data.get("name", "—")
|
||||
names.append(name)
|
||||
|
||||
enabled = "❌" if key in platform_disabled else "✅"
|
||||
ref_list = doc_refs.get(key, [])
|
||||
data_refs_attr = f' data-refs="{",".join(ref_list)}"' if ref_list else ""
|
||||
anchor = f'<span id="ref-{key}" class="entity-anchor"{data_refs_attr}></span>'
|
||||
row = f"| {anchor}`{key}` | " + " | ".join(names) + f" | {enabled} |"
|
||||
lines.append(row)
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_full_document(
|
||||
translations: dict[str, dict[str, dict[str, dict]]],
|
||||
disabled: dict[str, set[str]],
|
||||
) -> str:
|
||||
"""Generate the complete sensor-reference.md content."""
|
||||
doc_refs = scan_doc_refs()
|
||||
parts: list[str] = []
|
||||
|
||||
parts.append(FRONTMATTER)
|
||||
parts.append(INTRO)
|
||||
|
||||
# Sensors
|
||||
parts.append("## Sensors\n\n")
|
||||
parts.append(generate_table(SENSOR_CATEGORIES, "sensor", translations, disabled, doc_refs))
|
||||
|
||||
# Binary Sensors
|
||||
parts.append("## Binary Sensors\n\n")
|
||||
parts.append(generate_table(BINARY_SENSOR_CATEGORIES, "binary_sensor", translations, disabled, doc_refs))
|
||||
|
||||
# Number Entities
|
||||
parts.append("## Number Entities (Configuration Overrides)\n\n")
|
||||
parts.append(
|
||||
"> These entities allow runtime adjustment of period calculation parameters without "
|
||||
"changing the integration configuration. All are **disabled by default**.\n\n"
|
||||
)
|
||||
parts.append(generate_table(NUMBER_CATEGORIES, "number", translations, disabled, doc_refs))
|
||||
|
||||
# Switch Entities
|
||||
parts.append("## Switch Entities (Configuration Overrides)\n\n")
|
||||
parts.append(
|
||||
"> These switches control whether the relaxation algorithm is active for period detection. "
|
||||
"All are **disabled by default**.\n\n"
|
||||
)
|
||||
parts.append(generate_table(SWITCH_CATEGORIES, "switch", translations, disabled, doc_refs))
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Generate or check the sensor-reference.md file."""
|
||||
check_mode = "--check" in sys.argv
|
||||
|
||||
translations = load_translations()
|
||||
disabled = load_all_disabled()
|
||||
content = generate_full_document(translations, disabled)
|
||||
|
||||
if check_mode:
|
||||
if not OUTPUT_FILE.exists():
|
||||
print(f"✗ Sensor reference not found: {OUTPUT_FILE}")
|
||||
print(" Run: scripts/docs/generate-sensor-reference")
|
||||
return 1
|
||||
|
||||
existing = OUTPUT_FILE.read_text(encoding="utf-8")
|
||||
if existing == content:
|
||||
print("✓ Sensor reference is up to date")
|
||||
return 0
|
||||
print(f"✗ Sensor reference is outdated: {OUTPUT_FILE}")
|
||||
print(" Run: scripts/docs/generate-sensor-reference")
|
||||
return 1
|
||||
|
||||
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT_FILE.write_text(content, encoding="utf-8")
|
||||
|
||||
# Count entities
|
||||
total = 0
|
||||
for platform in ("sensor", "binary_sensor", "number", "switch"):
|
||||
count = len(translations.get("en", {}).get(platform, {}))
|
||||
total += count
|
||||
|
||||
print(f"✓ Generated {OUTPUT_FILE.relative_to(REPO_ROOT)} ({total} entities, {len(LANGUAGES)} languages)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -47,6 +47,36 @@ fi
|
|||
|
||||
"$SCRIPT_DIR/bootstrap"
|
||||
|
||||
# Setup Debian compatibility symlinks for common CLI tool names.
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
|
||||
# Generalized function to create command aliases for Debian compatibility
|
||||
ensure_command_alias() {
|
||||
local expected_cmd="$1"
|
||||
local actual_cmd="$2"
|
||||
|
||||
if command -v "$actual_cmd" >/dev/null 2>&1; then
|
||||
actual_path="$(command -v "$actual_cmd")"
|
||||
ln -sf "$actual_path" "$HOME/.local/bin/$expected_cmd" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Create command aliases for Debian compatibility
|
||||
# Debian package names differ from expected spellings
|
||||
ensure_command_alias "bat" "batcat"
|
||||
ensure_command_alias "fd" "fdfind"
|
||||
ensure_command_alias "fd-find" "fdfind"
|
||||
ensure_command_alias "delta" "git-delta"
|
||||
ensure_command_alias "http" "httpie"
|
||||
ensure_command_alias "ipython" "ipython3"
|
||||
ensure_command_alias "mlr" "miller"
|
||||
ensure_command_alias "rg" "ripgrep"
|
||||
|
||||
# Ensure user-local bin directory is available in interactive shells.
|
||||
if ! grep -q '\$HOME/.local/bin' "$HOME/.zshrc" 2>/dev/null; then
|
||||
printf '\n# Added by scripts/setup/setup\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$HOME/.zshrc"
|
||||
fi
|
||||
|
||||
# Install HACS for testing with other custom components
|
||||
echo ""
|
||||
log_header "Installing HACS in dev environment"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
"""Tests for Bug #6: Rating threshold validation in calculate_rating_level()."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from _pytest.logging import LogCaptureFixture
|
||||
|
||||
from custom_components.tibber_prices.utils.price import (
|
||||
_apply_rating_gap_tolerance,
|
||||
calculate_rating_level,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.logging import LogCaptureFixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def caplog_debug(caplog: LogCaptureFixture) -> LogCaptureFixture:
|
||||
|
|
|
|||
Loading…
Reference in a new issue