From 0e699ae14219a95b4764f170d1c254a8a9ad31d9 Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Sat, 11 Apr 2026 09:55:28 +0000 Subject: [PATCH] feat(docs): add multi-language entity reference with search Add a comprehensive entity reference system that helps users find entities across all 5 supported languages (EN, DE, NO, NL, SV). Core components: - Generator script (scripts/docs/generate-sensor-reference) that builds sensor-reference.md from translation files with --check mode for CI validation - EntityRef component for compact inline entity annotations with tooltip and version-aware linking to the reference table - EntitySearch component with live filtering, clickable results, keyboard navigation, "/" shortcut to focus, category filter chips, match highlighting, copy-entity-ID button per row, back-links to documentation pages, persistent row highlights, hash-based deep linking, and mobile-responsive layout - MDXComponents theme override for global component registration Documentation updates: - New sensor-reference.md page (115 entities x 5 languages) - EntityRef annotations across 10 documentation pages - Sidebar entry for quick navigation - CI integration (docusaurus.yml + scripts/check) - Ruff per-file-ignores for scripts/ (T201, INP001) Impact: Users can now find any entity by its localized display name regardless of their UI language. Inline EntityRef annotations link directly to the multi-language lookup table with version-aware URLs. --- .github/workflows/docusaurus.yml | 9 + docs/user/docs/actions.md | 22 +- docs/user/docs/automation-examples.md | 18 +- docs/user/docs/chart-examples.md | 12 +- docs/user/docs/configuration.md | 30 +- docs/user/docs/dashboard-examples.md | 2 +- docs/user/docs/dynamic-icons.md | 8 +- docs/user/docs/faq.md | 2 +- docs/user/docs/icon-colors.md | 10 +- docs/user/docs/period-calculation.md | 6 +- docs/user/docs/sensor-reference.md | 261 ++++++++++ docs/user/docs/sensors.md | 120 ++--- docs/user/sidebars.ts | 6 +- docs/user/src/components/EntityRef.tsx | 48 ++ docs/user/src/components/EntitySearch.tsx | 552 ++++++++++++++++++++ docs/user/src/css/custom.css | 434 ++++++++++++++++ docs/user/src/theme/MDXComponents.tsx | 13 + pyproject.toml | 6 +- scripts/check | 4 + scripts/docs/generate-sensor-reference | 581 ++++++++++++++++++++++ 20 files changed, 2024 insertions(+), 120 deletions(-) create mode 100644 docs/user/docs/sensor-reference.md create mode 100644 docs/user/src/components/EntityRef.tsx create mode 100644 docs/user/src/components/EntitySearch.tsx create mode 100644 docs/user/src/theme/MDXComponents.tsx create mode 100755 scripts/docs/generate-sensor-reference diff --git a/.github/workflows/docusaurus.yml b/.github/workflows/docusaurus.yml index 72f4280..adebd02 100644 --- a/.github/workflows/docusaurus.yml +++ b/.github/workflows/docusaurus.yml @@ -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 diff --git a/docs/user/docs/actions.md b/docs/user/docs/actions.md index c94b937..cdcfbb2 100644 --- a/docs/user/docs/actions.md +++ b/docs/user/docs/actions.md @@ -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:** `` 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 @@ -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,7 +296,7 @@ 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). diff --git a/docs/user/docs/automation-examples.md b/docs/user/docs/automation-examples.md index c0a6822..3eb59b0 100644 --- a/docs/user/docs/automation-examples.md +++ b/docs/user/docs/automation-examples.md @@ -19,7 +19,7 @@ > > These examples provide a good starting point but must be tailored to your individual Home Assistant setup. > -> **Entity ID tip:** `` 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:** `` 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 @@ -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?"** | `current_price_trend` state | +| **"When will the trend change?"** | `next_price_trend_change` 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?"** | `today_s_price_volatility` | | **"Ride the full cheap wave"** | `rating_level` + `current_price_trend` + `best_price_period` | --- @@ -513,11 +513,11 @@ automation: 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. +The examples below contain `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`. ::: ### Prerequisites @@ -542,7 +542,7 @@ The examples below contain `entry_id: YOUR_ENTRY_ID`. This value identifies whic # Generate configuration via automation/script service: tibber_prices.get_apexcharts_yaml data: - entry_id: YOUR_ENTRY_ID + entry_id: YOUR_CONFIG_ENTRY_ID day: today # or "yesterday", "tomorrow" level_type: rating_level # or "level" for 5-level view response_variable: apexcharts_config @@ -557,7 +557,7 @@ For a dynamic chart that automatically adapts to data availability: ```yaml service: tibber_prices.get_apexcharts_yaml data: - entry_id: YOUR_ENTRY_ID + entry_id: YOUR_CONFIG_ENTRY_ID day: rolling_window # Or omit for same behavior (default) level_type: rating_level response_variable: apexcharts_config @@ -576,7 +576,7 @@ For progressive zoom-in throughout the day: ```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 response_variable: apexcharts_config diff --git a/docs/user/docs/chart-examples.md b/docs/user/docs/chart-examples.md index 2648378..49ad602 100644 --- a/docs/user/docs/chart-examples.md +++ b/docs/user/docs/chart-examples.md @@ -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:** `` 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:** `` 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 diff --git a/docs/user/docs/configuration.md b/docs/user/docs/configuration.md index 0ed5d9d..7a15806 100644 --- a/docs/user/docs/configuration.md +++ b/docs/user/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration -> **Entity ID tip:** `` 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:** `` 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 @@ -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 | +| Best Price: Flexibility | Number | 0-50% | Maximum above daily minimum for "best price" intervals | +| Best Price: Minimum Distance | Number | -50-0% | Required distance below daily average | +| Best Price: Minimum Period Length | Number | 15-180 min | Shortest period duration to consider | +| Best Price: Minimum Periods | Number | 1-10 | Target number of periods per day | +| Best Price: Relaxation Attempts | Number | 1-12 | Steps to try when relaxing criteria | +| Best Price: Gap Tolerance | Number | 0-8 | Consecutive intervals allowed above threshold | +| Best Price: Achieve Minimum Count | Switch | On/Off | Enable relaxation algorithm | #### Peak Price Period Settings | Entity | Type | Range | Description | |--------|------|-------|-------------| -| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | -| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average | -| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider | -| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day | -| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria | -| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold | -| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm | +| Peak Price: Flexibility | Number | -50-0% | Maximum below daily maximum for "peak price" intervals | +| Peak Price: Minimum Distance | Number | 0-50% | Required distance above daily average | +| Peak Price: Minimum Period Length | Number | 15-180 min | Shortest period duration to consider | +| Peak Price: Minimum Periods | Number | 1-10 | Target number of periods per day | +| Peak Price: Relaxation Attempts | Number | 1-12 | Steps to try when relaxing criteria | +| Peak Price: Gap Tolerance | Number | 0-8 | Consecutive intervals allowed below threshold | +| Peak Price: Achieve Minimum Count | Switch | On/Off | Enable relaxation algorithm | ### How Runtime Overrides Work diff --git a/docs/user/docs/dashboard-examples.md b/docs/user/docs/dashboard-examples.md index d84c993..aa87856 100644 --- a/docs/user/docs/dashboard-examples.md +++ b/docs/user/docs/dashboard-examples.md @@ -2,7 +2,7 @@ Beautiful dashboard layouts using Tibber Prices sensors. -> **Entity ID tip:** `` 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:** `` 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 diff --git a/docs/user/docs/dynamic-icons.md b/docs/user/docs/dynamic-icons.md index ed53f24..8bcbe6c 100644 --- a/docs/user/docs/dynamic-icons.md +++ b/docs/user/docs/dynamic-icons.md @@ -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:** `` 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:** `` 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 diff --git a/docs/user/docs/faq.md b/docs/user/docs/faq.md index 260b09d..df0da71 100644 --- a/docs/user/docs/faq.md +++ b/docs/user/docs/faq.md @@ -112,7 +112,7 @@ If you see unexpected units, check your configuration in the integration options ## Automation Questions -> **Entity ID tip:** `` 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:** `` 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? diff --git a/docs/user/docs/icon-colors.md b/docs/user/docs/icon-colors.md index 6520394..b8dce54 100644 --- a/docs/user/docs/icon-colors.md +++ b/docs/user/docs/icon-colors.md @@ -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:** `` 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:** `` 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. diff --git a/docs/user/docs/period-calculation.md b/docs/user/docs/period-calculation.md index 91b8c0a..dfda2e9 100644 --- a/docs/user/docs/period-calculation.md +++ b/docs/user/docs/period-calculation.md @@ -2,7 +2,7 @@ Learn how Best Price and Peak Price periods work, and how to configure them for your needs. -> **Entity ID tip:** `` 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:** `` 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 @@ -28,8 +28,8 @@ Learn how Best Price and Peak Price periods work, and how to configure them for The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price): -- **Best Price Periods** 🟢 - When to run your dishwasher, charge your EV, or heat water -- **Peak Price Periods** 🔴 - When to reduce consumption or defer non-essential loads +- Best Price Periods 🟢 - When to run your dishwasher, charge your EV, or heat water +- Peak Price Periods 🔴 - When to reduce consumption or defer non-essential loads ### Default Behavior diff --git a/docs/user/docs/sensor-reference.md b/docs/user/docs/sensor-reference.md new file mode 100644 index 0000000..b4719d0 --- /dev/null +++ b/docs/user/docs/sensor-reference.md @@ -0,0 +1,261 @@ +--- +comments: false +--- + +# Entity Reference (All Languages) + + + +## How to Find Your Entity in Home Assistant + +**Entity ID pattern:** `sensor._` + +- `` is generated from your Tibber home display name (lowercase, spaces replaced with underscores) +- `` 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 Guide](sensors.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 | +|---|---|---|---|---|---|---| +| `current_interval_price` | Current Electricity Price | Aktueller Strompreis | Nåværende strømpris | Huidige Elektriciteitsprijs | Aktuellt elpris | ✅ | +| `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) | ✅ | +| `next_interval_price` | Next Electricity Price | Nächster Strompreis | Neste strømpris | Volgende Elektriciteitsprijs | Nästa elpris | ✅ | +| `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 | +|---|---|---|---|---|---|---| +| `current_hour_average_price` | ⌀ Hourly Price Current | ⌀ Stunden-Preis aktuell | ⌀ Timepris nåværende | ⌀ Uurprijs Huidig | ⌀ Timpris aktuell | ✅ | +| `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 | +|---|---|---|---|---|---|---| +| `lowest_price_today` | Today's Lowest Price | Mindestpreis heute | Dagens laveste pris | Laagste Prijs Vandaag | Dagens lägsta pris | ✅ | +| `highest_price_today` | Today's Highest Price | Höchstpreis heute | Dagens høyeste pris | Hoogste Prijs Vandaag | Dagens högsta pris | ✅ | +| `average_price_today` | ⌀ Price Today | ⌀ Preis heute | ⌀ Pris i dag | ⌀ Prijs Vandaag | ⌀ Pris idag | ✅ | +| `lowest_price_tomorrow` | Tomorrow's Lowest Price | Mindestpreis morgen | Morgendagens laveste pris | Laagste Prijs Morgen | Morgondagens lägsta pris | ✅ | +| `highest_price_tomorrow` | Tomorrow's Highest Price | Höchstpreis morgen | Morgendagens høyeste pris | Hoogste Prijs Morgen | Morgondagens högsta pris | ✅ | +| `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 | +|---|---|---|---|---|---|---| +| `trailing_price_average` | ⌀ Price Trailing 24h | ⌀ Preis nachlaufend 24h | ⌀ Pris glidende 24t | ⌀ Prijs Afgelopen 24u | ⌀ Pris glidande 24h | ❌ | +| `leading_price_average` | ⌀ Price Leading 24h | ⌀ Preis vorlaufend 24h | ⌀ Pris fremtidig 24t | ⌀ Prijs Komende 24u | ⌀ Pris framåt 24h | ❌ | +| `trailing_price_min` | Trailing 24h Minimum Price | 24h-Mindestpreis nachlaufend | Glidende 24t minimumspris | Afgelopen 24u Minimumprijs | Glidande 24h minimipris | ❌ | +| `trailing_price_max` | Trailing 24h Maximum Price | 24h-Höchstpreis nachlaufend | Glidende 24t maksimumspris | Afgelopen 24u Maximumprijs | Glidande 24h maximipris | ❌ | +| `leading_price_min` | Leading 24h Minimum Price | 24h-Mindestpreis vorlaufend | Fremtidig 24t minimumspris | Komende 24u Minimumprijs | Framåt 24h minimipris | ❌ | +| `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 | +|---|---|---|---|---|---|---| +| `next_avg_1h` | ⌀ Price Next 1h | ⌀ Preis nächste 1h | ⌀ Pris neste 1t | ⌀ Prijs Komende 1u | ⌀ Pris nästa 1h | ✅ | +| `next_avg_2h` | ⌀ Price Next 2h | ⌀ Preis nächste 2h | ⌀ Pris neste 2t | ⌀ Prijs Komende 2u | ⌀ Pris nästa 2h | ✅ | +| `next_avg_3h` | ⌀ Price Next 3h | ⌀ Preis nächste 3h | ⌀ Pris neste 3t | ⌀ Prijs Komende 3u | ⌀ Pris nästa 3h | ✅ | +| `next_avg_4h` | ⌀ Price Next 4h | ⌀ Preis nächste 4h | ⌀ Pris neste 4t | ⌀ Prijs Komende 4u | ⌀ Pris nästa 4h | ✅ | +| `next_avg_5h` | ⌀ Price Next 5h | ⌀ Preis nächste 5h | ⌀ Pris neste 5t | ⌀ Prijs Komende 5u | ⌀ Pris nästa 5h | ✅ | +| `next_avg_6h` | ⌀ Price Next 6h | ⌀ Preis nächste 6h | ⌀ Pris neste 6t | ⌀ Prijs Komende 6u | ⌀ Pris nästa 6h | ❌ | +| `next_avg_8h` | ⌀ Price Next 8h | ⌀ Preis nächste 8h | ⌀ Pris neste 8t | ⌀ Prijs Komende 8u | ⌀ Pris nästa 8h | ❌ | +| `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 | +|---|---|---|---|---|---|---| +| `current_interval_price_level` | Current Price Level | Aktuelles Preisniveau | Nåværende prisnivå | Huidig Prijsniveau | Aktuell prisnivå | ✅ | +| `next_interval_price_level` | Next Price Level | Nächstes Preisniveau | Neste prisnivå | Volgend Prijsniveau | Nästa prisnivå | ✅ | +| `previous_interval_price_level` | Previous Price Level | Vorheriges Preisniveau | Forrige prisnivå | Vorig Prijsniveau | Föregående prisnivå | ❌ | +| `current_hour_price_level` | Current Hour Price Level | Aktuelles Stunden-Preisniveau | Nåværende timepris nivå | Huidig Uur Prijsniveau | Aktuell timprisnivå | ✅ | +| `next_hour_price_level` | Next Hour Price Level | Nächstes Stunden-Preisniveau | Neste timepris nivå | Volgend Uur Prijsniveau | Nästa timprisnivå | ✅ | +| `yesterday_price_level` | Yesterday's Price Level | Preisniveau gestern | Prisnivå i går | Gisteren Prijsniveau | Gårdagens prisnivå | ❌ | +| `today_price_level` | Today's Price Level | Preisniveau heute | Prisnivå i dag | Vandaag Prijsniveau | Dagens prisnivå | ✅ | +| `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 | +|---|---|---|---|---|---|---| +| `current_interval_price_rating` | Current Price Rating | Aktuelle Preisbewertung | Nåværende prisvurdering | Huidige Prijsbeoordeling | Aktuellt prisbetyg | ❌ | +| `next_interval_price_rating` | Next Price Rating | Nächste Preisbewertung | Neste prisvurdering | Volgende Prijsbeoordeling | Nästa prisbetyg | ❌ | +| `previous_interval_price_rating` | Previous Price Rating | Vorherige Preisbewertung | Forrige prisvurdering | Vorige Prijsbeoordeling | Föregående prisbetyg | ❌ | +| `current_hour_price_rating` | Current Hour Price Rating | Aktuelle Stunden-Preisbewertung | Nåværende timeprisvurdering | Huidig Uur Prijsbeoordeling | Aktuellt timprisbetyg | ❌ | +| `next_hour_price_rating` | Next Hour Price Rating | Nächste Stunden-Preisbewertung | Neste timeprisvurdering | Volgend Uur Prijsbeoordeling | Nästa timprisbetyg | ❌ | +| `yesterday_price_rating` | Yesterday's Price Rating | Preisbewertung gestern | Prisvurdering i går | Gisteren Prijsbeoordeling | Gårdagens prisbetyg | ❌ | +| `today_price_rating` | Today's Price Rating | Preisbewertung heute | Prisvurdering i dag | Vandaag Prijsbeoordeling | Dagens prisbetyg | ❌ | +| `tomorrow_price_rating` | Tomorrow's Price Rating | Preisbewertung morgen | Prisvurdering i morgen | Morgen Prijsbeoordeling | Morgondagens prisbetyg | ❌ | +| `daily_rating` | Daily Price Rating | Tägliche Preisbewertung | Daglig prisvurdering | Dagelijkse Prijsbeoordeling | Dagligt prisbetyg | ✅ | +| `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 | +|---|---|---|---|---|---|---| +| `current_price_trend` | Current Price Trend | Aktueller Preistrend | Nåværende pristrend | Huidige Prijstrend | Aktuell pristrend | ✅ | +| `next_price_trend_change` | Next Price Trend Change | Nächste Trendänderung | Neste trendendring | Volgende Prijstrend Wijziging | Nästa pristrendändring | ✅ | +| `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 | ✅ | +| `price_outlook_1h` | Price Outlook (1h) | Preisausblick (1h) | Prisutblikk (1t) | Prijsvooruitzicht (1u) | Prisöversikt (1h) | ✅ | +| `price_outlook_2h` | Price Outlook (2h) | Preisausblick (2h) | Prisutblikk (2t) | Prijsvooruitzicht (2u) | Prisöversikt (2h) | ✅ | +| `price_outlook_3h` | Price Outlook (3h) | Preisausblick (3h) | Prisutblikk (3t) | Prijsvooruitzicht (3u) | Prisöversikt (3h) | ✅ | +| `price_outlook_4h` | Price Outlook (4h) | Preisausblick (4h) | Prisutblikk (4t) | Prijsvooruitzicht (4u) | Prisöversikt (4h) | ✅ | +| `price_outlook_5h` | Price Outlook (5h) | Preisausblick (5h) | Prisutblikk (5t) | Prijsvooruitzicht (5u) | Prisöversikt (5h) | ✅ | +| `price_outlook_6h` | Price Outlook (6h) | Preisausblick (6h) | Prisutblikk (6t) | Prijsvooruitzicht (6u) | Prisöversikt (6h) | ❌ | +| `price_outlook_8h` | Price Outlook (8h) | Preisausblick (8h) | Prisutblikk (8t) | Prijsvooruitzicht (8u) | Prisöversikt (8h) | ❌ | +| `price_outlook_12h` | Price Outlook (12h) | Preisausblick (12h) | Prisutblikk (12t) | Prijsvooruitzicht (12u) | Prisöversikt (12h) | ❌ | +| `price_trajectory_2h` | Price Trajectory (2h) | Preisverlauf (2h) | Prisforløp (2t) | Prijstrajectorie (2u) | Prisutveckling (2h) | ✅ | +| `price_trajectory_3h` | Price Trajectory (3h) | Preisverlauf (3h) | Prisforløp (3t) | Prijstrajectorie (3u) | Prisutveckling (3h) | ✅ | +| `price_trajectory_4h` | Price Trajectory (4h) | Preisverlauf (4h) | Prisforløp (4t) | Prijstrajectorie (4u) | Prisutveckling (4h) | ✅ | +| `price_trajectory_5h` | Price Trajectory (5h) | Preisverlauf (5h) | Prisforløp (5t) | Prijstrajectorie (5u) | Prisutveckling (5h) | ✅ | +| `price_trajectory_6h` | Price Trajectory (6h) | Preisverlauf (6h) | Prisforløp (6t) | Prijstrajectorie (6u) | Prisutveckling (6h) | ❌ | +| `price_trajectory_8h` | Price Trajectory (8h) | Preisverlauf (8h) | Prisforløp (8t) | Prijstrajectorie (8u) | Prisutveckling (8h) | ❌ | +| `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 | +|---|---|---|---|---|---|---| +| `today_volatility` | Today's Price Volatility | Volatilität heute | Volatilitet i dag | Vandaag Prijsvolatiliteit | Dagens prisvolatilitet | ✅ | +| `tomorrow_volatility` | Tomorrow's Price Volatility | Volatilität morgen | Volatilitet i morgen | Morgen Prijsvolatiliteit | Morgondagens prisvolatilitet | ❌ | +| `next_24h_volatility` | Next 24h Price Volatility | Volatilität der nächsten 24h | Volatilitet neste 24t | Komende 24u Prijsvolatiliteit | Nästa 24h prisvolatilitet | ❌ | +| `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 | +|---|---|---|---|---|---|---| +| `best_price_end_time` | Best Price End | Bestpreis endet | Beste pris slutter | Beste Prijs Einde | Bästa pris slutar | ✅ | +| `best_price_period_duration` | Best Price Duration | Bestpreis Dauer | Beste pris varighet | Beste Prijs Duur | Bästa pris varaktighet | ❌ | +| `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 | ✅ | +| `best_price_progress` | Best Price Progress | Bestpreis Fortschritt | Beste pris fremgang | Beste Prijs Voortgang | Bästa pris framsteg | ✅ | +| `best_price_next_start_time` | Best Price Start | Bestpreis startet | Beste pris starter | Beste Prijs Start | Bästa pris startar | ✅ | +| `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 | +|---|---|---|---|---|---|---| +| `peak_price_end_time` | Peak Price End | Spitzenpreis endet | Topppris slutter | Piekprijs Einde | Topppris slutar | ✅ | +| `peak_price_period_duration` | Peak Price Duration | Spitzenpreis Dauer | Topppris varighet | Piekprijs Duur | Topppris varaktighet | ❌ | +| `peak_price_remaining_minutes` | Peak Price Remaining Time | Spitzenpreis verbleibend | Topppris gjenværende tid | Piekprijs Resterende Tijd | Topppris återstående tid | ✅ | +| `peak_price_progress` | Peak Price Progress | Spitzenpreis Fortschritt | Topppris fremgang | Piekprijs Voortgang | Topppris framsteg | ✅ | +| `peak_price_next_start_time` | Peak Price Start | Spitzenpreis startet | Topppris starter | Piekprijs Start | Topppris startar | ✅ | +| `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 | +|---|---|---|---|---|---|---| +| `home_type` | Home Type | Wohnungstyp | Boligtype | Huistype | Hemtyp | ❌ | +| `home_size` | Home Size | Wohnfläche | Boligareal | Huisgrootte | Hemstorlek | ❌ | +| `main_fuse_size` | Main Fuse Size | Hauptsicherung | Hovedsikring | Hoofdzekering Grootte | Huvudsäkringsstorlek | ❌ | +| `number_of_residents` | Number of Residents | Anzahl Bewohner | Antall beboere | Aantal Bewoners | Antal boende | ❌ | +| `primary_heating_source` | Primary Heating Source | Primäre Heizquelle | Primær varmekilde | Primaire Verwarmingsbron | Primär värmekälla | ❌ | +| `grid_company` | Grid Company | Netzbetreiber | Nettselskap | Netbedrijf | Nätbolag | ✅ | +| `grid_area_code` | Grid Area Code | Netzgebietscode | Nettområdekode | Netgebiedcode | Nätområdeskod | ❌ | +| `price_area_code` | Price Area Code | Preiszonencode | Prisområdekode | Prijsgebiedcode | Prisområdeskod | ❌ | +| `consumption_ean` | Consumption EAN | Verbrauchs-EAN | Forbruks-EAN | Verbruik EAN | Förbruknings-EAN | ❌ | +| `production_ean` | Production EAN | Erzeugungs-EAN | Produksjons-EAN | Productie EAN | Produktions-EAN | ❌ | +| `energy_tax_type` | Energy Tax Type | Energiesteuertyp | Energiavgiftstype | Energiebelasting Type | Energiskattetyp | ❌ | +| `vat_type` | VAT Type | Mehrwertsteuertyp | MVA-type | BTW Type | Momstyp | ❌ | +| `estimated_annual_consumption` | Estimated Annual Consumption | Geschätzter Jahresverbrauch | Estimert årlig forbruk | Geschat Jaarverbruik | Beräknad årlig förbrukning | ✅ | +| `subscription_status` | Subscription Status | Abonnementstatus | Abonnementsstatus | Abonnement Status | Abonnemangsstatus | ❌ | + +### Data & Diagnostics + + +| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default | +|---|---|---|---|---|---|---| +| `data_lifecycle_status` | Data Lifecycle Status | Datenlebenszyklus-Status | Datalivssyklus-status | Data Levenscyclus Status | Datalivscykelstatus | ✅ | +| `chart_data_export` | Chart Data Export | Diagramm-Datenexport | Diagramdataeksport | Grafiekdata Export | Diagramdataexport | ❌ | +| `chart_metadata` | Chart Metadata | Diagramm-Metadaten | Diagrammetadata | Grafiek Metadata | Diagrammetadata | ✅ | +## Binary Sensors + +### Binary Sensors + + +| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default | +|---|---|---|---|---|---|---| +| `best_price_period` | Best Price Period | Bestpreis-Zeitraum | Lavpris-periode | Beste Prijs Periode | Bästa Prisperiod | ✅ | +| `peak_price_period` | Peak Price Period | Spitzenpreis-Zeitraum | Toppris-periode | Piekprijs Periode | Topprisperiod | ✅ | +| `connection` | Tibber API Connection | Tibber-API-Verbindung | Tibber API-tilkobling | Tibber API Verbinding | Tibber API-anslutning | ✅ | +| `tomorrow_data_available` | Tomorrow's Data Available | Morgige Daten verfügbar | Morgendagens data tilgjengelig | Morgen Gegevens Beschikbaar | Morgondagens data tillgänglig | ✅ | +| `has_ventilation_system` | Has Ventilation System | Hat Lüftungsanlage | Har ventilasjonsanlegg | Heeft Ventilatiesysteem | Har ventilationssystem | ❌ | +| `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 | +|---|---|---|---|---|---|---| +| `best_price_flex_override` | Best Price: Flexibility | Bestpreis: Flexibilität | Beste pris: Fleksibilitet | Beste prijs: Flexibiliteit | Bästa pris: Flexibilitet | ❌ | +| `best_price_min_distance_override` | Best Price: Minimum Distance | Bestpreis: Mindestabstand | Beste pris: Minimumsavstand | Beste prijs: Minimale afstand | Bästa pris: Minimiavstånd | ❌ | +| `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 | ❌ | +| `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 | ❌ | +| `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 | ❌ | +| `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 | +|---|---|---|---|---|---|---| +| `peak_price_flex_override` | Peak Price: Flexibility | Spitzenpreis: Flexibilität | Topppris: Fleksibilitet | Piekprijs: Flexibiliteit | Topppris: Flexibilitet | ❌ | +| `peak_price_min_distance_override` | Peak Price: Minimum Distance | Spitzenpreis: Mindestabstand | Topppris: Minimumsavstand | Piekprijs: Minimale afstand | Topppris: Minimiavstånd | ❌ | +| `peak_price_min_period_length_override` | Peak Price: Minimum Period Length | Spitzenpreis: Mindestperiodenlänge | Topppris: Minimum periodelengde | Piekprijs: Minimale periodelengte | Topppris: Minsta periodlängd | ❌ | +| `peak_price_min_periods_override` | Peak Price: Minimum Periods | Spitzenpreis: Mindestperioden | Topppris: Minimum perioder | Piekprijs: Minimum periodes | Topppris: Minsta antal perioder | ❌ | +| `peak_price_relaxation_attempts_override` | Peak Price: Relaxation Attempts | Spitzenpreis: Lockerungsversuche | Topppris: Lemping forsøk | Piekprijs: Versoepeling pogingen | Topppris: Lättnadsförsök | ❌ | +| `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 | +|---|---|---|---|---|---|---| +| `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 | ❌ | +| `peak_price_enable_relaxation_override` | Peak Price: Achieve Minimum Count | Spitzenpreis: Mindestanzahl erreichen | Topppris: Oppnå minimumsantall | Piekprijs: Minimum aantal bereiken | Topppris: Uppnå minimiantal | ❌ | diff --git a/docs/user/docs/sensors.md b/docs/user/docs/sensors.md index 2cfa73c..f4c4aa8 100644 --- a/docs/user/docs/sensors.md +++ b/docs/user/docs/sensors.md @@ -6,7 +6,7 @@ comments: false > **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:** `` 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:** `` 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. ## Binary Sensors @@ -16,8 +16,8 @@ These binary sensors indicate when you're in a detected best or peak price perio **Quick overview:** -- **Best Price Period**: Turns ON during periods with significantly lower prices than the daily average -- **Peak Price Period**: Turns ON during periods with significantly higher prices than the daily average +- Best Price Period: Turns ON during periods with significantly lower prices than the daily average +- Peak Price Period: Turns ON during periods with significantly higher prices than the daily average Both sensors include rich attributes with period details, intervals, relaxation status, and more. @@ -31,13 +31,13 @@ The integration provides several sensors that calculate average electricity pric | Sensor | Description | Time Window | |--------|-------------|-------------| -| **Average Price Today** | Typical price for current calendar day | 00:00 - 23:59 today | -| **Average Price Tomorrow** | Typical price for next calendar day | 00:00 - 23:59 tomorrow | -| **Trailing Price Average** | Typical price for last 24 hours | Rolling 24h backward | -| **Leading Price Average** | Typical price for next 24 hours | Rolling 24h forward | -| **Current Hour Average** | Smoothed price around current time | 5 intervals (~75 min) | -| **Next Hour Average** | Smoothed price around next hour | 5 intervals (~75 min) | -| **Next N Hours Average** | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | +| Average Price Today | Typical price for current calendar day | 00:00 - 23:59 today | +| Average Price Tomorrow | Typical price for next calendar day | 00:00 - 23:59 tomorrow | +| Trailing Price Average | Typical price for last 24 hours | Rolling 24h backward | +| Leading Price Average | Typical price for next 24 hours | Rolling 24h forward | +| Current Hour Average | Smoothed price around current time | 5 intervals (~75 min) | +| Next Hour Average | Smoothed price around next hour | 5 intervals (~75 min) | +| **Next N Hours Average** (`next_avg_1h`–`next_avg_12h`) | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h | #### Configurable Display: Median vs Mean @@ -234,10 +234,10 @@ The sensor's state can be `low`, `moderate`, `high`, or `very_high`, based on co | Sensor | Description | Time Window | |---|---|---| -| **Today's Price Volatility** | Volatility for the current calendar day | 00:00 - 23:59 today | -| **Tomorrow's Price Volatility** | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | -| **Next 24h Price Volatility** | Volatility for the next 24 hours from now | Rolling 24h forward | -| **Today + Tomorrow Price Volatility** | Volatility across both today and tomorrow | Up to 48 hours | +| Today's Price Volatility | Volatility for the current calendar day | 00:00 - 23:59 today | +| Tomorrow's Price Volatility | Volatility for the next calendar day | 00:00 - 23:59 tomorrow | +| **Next 24h Price Volatility** (`next_24h_volatility`) | Volatility for the next 24 hours from now | Rolling 24h forward | +| Today + Tomorrow Price Volatility | Volatility across both today and tomorrow | Up to 48 hours | ### Configuration @@ -358,14 +358,14 @@ stateDiagram-v2 | Sensor | Scope | Description | |--------|-------|-------------| -| **Current Price Rating** | Current interval | Rating of the current 15-minute price | -| **Next Price Rating** | Next interval | Rating for the upcoming 15-minute price | -| **Previous Price Rating** | Previous interval | Rating for the past 15-minute price | -| **Current Hour Price Rating** | Rolling 5-interval | Smoothed rating around the current hour | -| **Next Hour Price Rating** | Rolling 5-interval | Smoothed rating around the next hour | -| **Yesterday's Price Rating** | Calendar day | Aggregated rating for yesterday | -| **Today's Price Rating** | Calendar day | Aggregated rating for today | -| **Tomorrow's Price Rating** | Calendar day | Aggregated rating for tomorrow | +| Current Price Rating | Current interval | Rating of the current 15-minute price | +| Next Price Rating | Next interval | Rating for the upcoming 15-minute price | +| Previous Price Rating | Previous interval | Rating for the past 15-minute price | +| Current Hour Price Rating | Rolling 5-interval | Smoothed rating around the current hour | +| Next Hour Price Rating | Rolling 5-interval | Smoothed rating around the next hour | +| Yesterday's Price Rating | Calendar day | Aggregated rating for yesterday | +| Today's Price Rating | Calendar day | Aggregated rating for today | +| Tomorrow's Price Rating | Calendar day | Aggregated rating for tomorrow | ### Ratings vs Levels @@ -438,14 +438,14 @@ Level sensors show the **Tibber API's own price classification** with a 5-level | Sensor | Scope | |--------|-------| -| **Current Price Level** | Current interval | -| **Next Price Level** | Next interval | -| **Previous Price Level** | Previous interval | -| **Current Hour Price Level** | Rolling 5-interval window | -| **Next Hour Price Level** | Rolling 5-interval window | -| **Yesterday's Price Level** | Calendar day (aggregated) | -| **Today's Price Level** | Calendar day (aggregated) | -| **Tomorrow's Price Level** | Calendar day (aggregated) | +| Current Price Level | Current interval | +| Next Price Level | Next interval | +| Previous Price Level | Previous interval | +| Current Hour Price Level | Rolling 5-interval window | +| Next Hour Price Level | Rolling 5-interval window | +| Yesterday's Price Level | Calendar day (aggregated) | +| Today's Price Level | Calendar day (aggregated) | +| Tomorrow's Price Level | Calendar day (aggregated) | **Gap tolerance** smoothing is applied to prevent isolated level flickers (e.g., a single NORMAL between two CHEAPs → corrected to CHEAP). Configure in [options flow](configuration.md#step-4-price-level-gap-tolerance). @@ -457,19 +457,19 @@ These sensors show the lowest and highest prices for calendar days and rolling w | Sensor | Description | |--------|-------------| -| **Today's Lowest Price** | Minimum price today (00:00–23:59) | -| **Today's Highest Price** | Maximum price today (00:00–23:59) | -| **Tomorrow's Lowest Price** | Minimum price tomorrow | -| **Tomorrow's Highest Price** | Maximum price tomorrow | +| Today's Lowest Price | Minimum price today (00:00–23:59) | +| Today's Highest Price | Maximum price today (00:00–23:59) | +| Tomorrow's Lowest Price | Minimum price tomorrow | +| Tomorrow's Highest Price | Maximum price tomorrow | ### 24-Hour Rolling Min/Max | Sensor | Description | |--------|-------------| -| **Trailing Price Min** | Lowest price in the last 24 hours | -| **Trailing Price Max** | Highest price in the last 24 hours | -| **Leading Price Min** | Lowest price in the next 24 hours | -| **Leading Price Max** | Highest price in the next 24 hours | +| Trailing Price Min | Lowest price in the last 24 hours | +| Trailing Price Max | Highest price in the last 24 hours | +| Leading Price Min | Lowest price in the next 24 hours | +| Leading Price Max | Highest price in the next 24 hours | ### Key Attributes @@ -608,12 +608,12 @@ For each period type (Best Price and Peak Price): | Sensor | When Period Active | When No Active Period | |--------|-------------------|----------------------| -| **End Time** | Current period's end time | Next period's end time | -| **Period Duration** | Current period length (minutes) | Next period length | -| **Remaining Minutes** | Minutes until current period ends | 0 | -| **Progress** | 0–100% through current period | 0 | -| **Next Start Time** | When next-next period starts | When next period starts | -| **Next In Minutes** | Minutes to next-next period | Minutes to next period | +| End Time | Current period's end time | Next period's end time | +| Period Duration | Current period length (minutes) | Next period length | +| Remaining Minutes | Minutes until current period ends | 0 | +| Progress | 0–100% through current period | 0 | +| Next Start Time | When next-next period starts | When next period starts | +| Next In Minutes | Minutes to next-next period | Minutes to next period | ### Usage Examples @@ -689,14 +689,14 @@ These sensors compare the **current price** with the **average price** of the ne | Sensor | Compares Against | |--------|-----------------| -| **Price Outlook (1h)** | Average of next 1 hour | -| **Price Outlook (2h)** | Average of next 2 hours | -| **Price Outlook (3h)** | Average of next 3 hours | -| **Price Outlook (4h)** | Average of next 4 hours | -| **Price Outlook (5h)** | Average of next 5 hours | -| **Price Outlook (6h)** | Average of next 6 hours | -| **Price Outlook (8h)** | Average of next 8 hours | -| **Price Outlook (12h)** | Average of next 12 hours | +| **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. @@ -761,13 +761,13 @@ These sensors compare the **first half** of the future window against the **seco | Sensor | Compares | |--------|----------| -| **Price Trajectory (2h)** | Avg of hour 1 vs avg of hour 2 | -| **Price Trajectory (3h)** | Avg of first 1.5h vs avg of second 1.5h | -| **Price Trajectory (4h)** | Avg of first 2h vs avg of second 2h | -| **Price Trajectory (5h)** | Avg of first 2.5h vs avg of second 2.5h | -| **Price Trajectory (6h)** | Avg of first 3h vs avg of second 3h | -| **Price Trajectory (8h)** | Avg of first 4h vs avg of second 4h | -| **Price Trajectory (12h)** | Avg of first 6h vs avg of second 6h | +| **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`). @@ -1031,7 +1031,7 @@ If you're currently using this sensor, consider migrating to the service: # New approach (service) - 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 diff --git a/docs/user/sidebars.ts b/docs/user/sidebars.ts index f85e7b0..2c6ca8f 100644 --- a/docs/user/sidebars.ts +++ b/docs/user/sidebars.ts @@ -32,7 +32,7 @@ const sidebars: SidebarsConfig = { { type: 'category', label: '📊 Features', - items: ['sensors', 'period-calculation', 'dynamic-icons', 'icon-colors', 'actions'], + items: ['sensors', 'sensor-reference', 'period-calculation', 'dynamic-icons', 'icon-colors', 'actions'], collapsible: true, collapsed: false, }, @@ -52,14 +52,14 @@ const sidebars: SidebarsConfig = { }, { type: 'category', - label: '� Community', + label: '👥 Community', items: ['community-examples'], collapsible: true, collapsed: false, }, { type: 'category', - label: '�🔧 Help & Support', + label: '🔧 Help & Support', items: ['faq', 'troubleshooting'], collapsible: true, collapsed: false, diff --git a/docs/user/src/components/EntityRef.tsx b/docs/user/src/components/EntityRef.tsx new file mode 100644 index 0000000..539659d --- /dev/null +++ b/docs/user/src/components/EntityRef.tsx @@ -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 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: + * Average Price Today + * End Time + */ +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 : {children}; + + return ( + + {content} + + ); +} diff --git a/docs/user/src/components/EntitySearch.tsx b/docs/user/src/components/EntitySearch.tsx new file mode 100644 index 0000000..2de5b9f --- /dev/null +++ b/docs/user/src/components/EntitySearch.tsx @@ -0,0 +1,552 @@ +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 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 = { + sensors: 'Sensors Guide', + configuration: 'Configuration', + 'period-calculation': 'Period Calculation', + '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 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, '$1'); + }); +} + +/** + * 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([]); + const [activeIndex, setActiveIndex] = useState(-1); + const [activeChip, setActiveChip] = useState(null); + const entriesRef = useRef([]); + const containerRef = useRef(null); + const inputRef = useRef(null); + const resultsRef = useRef(null); + + // ── Build the search index on mount ────────────────────────── + useEffect(() => { + const anchors = document.querySelectorAll('.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((slug) => { + const a = document.createElement('a'); + a.href = slug; + 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(); + + // 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) => { + 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) => { + 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 ( +
+ {/* ── Category filter chips ── */} +
+ {PLATFORM_CHIPS.map((chip) => ( + + ))} + + Press / to search + +
+ + {/* ── Search input ── */} +
+ + 0} + aria-controls="entity-search-results" + aria-activedescendant={ + activeIndex >= 0 ? `entity-result-${activeIndex}` : undefined + } + autoComplete="off" + spellCheck={false} + role="combobox" + /> + {isFiltering && ( + + )} +
+ + {/* ── Results dropdown ── */} + {isFiltering && ( +
+ {matchCount === 0 ? ( +
+ No matching entities found +
+ ) : ( + <> + {query.trim().length > 0 && ( +
    + {visibleMatches.map((entry, i) => { + const matchedTranslation = getMatchingName(entry, query); + return ( +
  • scrollToEntry(entry)} + onMouseEnter={() => setActiveIndex(i)} + > + + {entry.englishName} + + {entry.key} + {matchedTranslation && ( + + {matchedTranslation} + + )} +
  • + ); + })} +
+ )} +
+ {matchCount} of {total} entities + {hasMore && query.trim().length > 0 && ( + + {' '}— showing first {MAX_RESULTS}, type more to narrow down + + )} +
+ + )} +
+ )} +
+ ); +} diff --git a/docs/user/src/css/custom.css b/docs/user/src/css/custom.css index 0eaadc5..8db3415 100644 --- a/docs/user/src/css/custom.css +++ b/docs/user/src/css/custom.css @@ -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; diff --git a/docs/user/src/theme/MDXComponents.tsx b/docs/user/src/theme/MDXComponents.tsx new file mode 100644 index 0000000..2d28fb4 --- /dev/null +++ b/docs/user/src/theme/MDXComponents.tsx @@ -0,0 +1,13 @@ +/** + * Extend the default MDXComponents so that and + * 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, +}; diff --git a/pyproject.toml b/pyproject.toml index d94ee0f..aaab2b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] @@ -46,6 +46,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 diff --git a/scripts/check b/scripts/check index c08b039..167944c 100755 --- a/scripts/check +++ b/scripts/check @@ -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" diff --git a/scripts/docs/generate-sensor-reference b/scripts/docs/generate-sensor-reference new file mode 100755 index 0000000..eaacfb0 --- /dev/null +++ b/scripts/docs/generate-sensor-reference @@ -0,0 +1,581 @@ +#!/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 scan_doc_refs() -> dict[str, list[str]]: + """ + Scan doc markdown files for EntityRef usage. + + Returns: {entity_key: [doc_slug, ...]} + """ + refs: dict[str, list[str]] = {} + docs_dir = REPO_ROOT / "docs" / "user" / "docs" + entity_ref_pattern = re.compile(r']*?\bid="([^"]+)"') + also_pattern = re.compile(r'\balso="([^"]+)"') + + 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") + for match in entity_ref_pattern.finditer(text): + key = match.group(1) + refs.setdefault(key, []) + if slug not in refs[key]: + refs[key].append(slug) + # 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: + also_key = also_match.group(1) + refs.setdefault(also_key, []) + if slug not in refs[also_key]: + refs[also_key].append(slug) + + return refs + + +# --------------------------------------------------------------------------- +# Markdown generation +# --------------------------------------------------------------------------- + +FRONTMATTER = """\ +--- +comments: false +--- + +""" + +INTRO = """\ +# Entity Reference (All Languages) + + + +## How to Find Your Entity in Home Assistant + +**Entity ID pattern:** `sensor._` + +- `` is generated from your Tibber home display name (lowercase, spaces replaced with underscores) +- `` 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 Guide](sensors.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'' + 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'' + 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())