mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
refactor(repairs): simplify currency mode change notification to one-shot
Remove the DATA_STATISTICS_REVIEW_REQUIRED flag and all associated persistence logic. The flag approach was over-engineered: we cannot detect whether the Recorder statistics have been fixed, and requiring the user to re-save display settings as acknowledgement is bad UX. New design: show the repair notice once when the mode changes. The user dismisses it when done reviewing. The HA Recorder will independently show its own unit-change dialog — that is sufficient. Changes: - Remove DATA_STATISTICS_REVIEW_REQUIRED constant from const.py - Remove _check_statistics_review_repair() from __init__.py - Remove ir import from __init__.py (no longer needed there) - Remove flag set/clear logic from options_flow.py - Change is_persistent=False (no restart persistence needed) - Update all 5 translations: restore simple "Dismiss this notice" ending
This commit is contained in:
parent
09edcdb9a3
commit
7629c0f628
24 changed files with 1044 additions and 75 deletions
|
|
@ -14,7 +14,6 @@ import voluptuous as vol
|
|||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.loader import async_get_loaded_integration
|
||||
|
|
@ -26,7 +25,6 @@ from .const import (
|
|||
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
DATA_CHART_CONFIG,
|
||||
DATA_CHART_METADATA_CONFIG,
|
||||
DATA_STATISTICS_REVIEW_REQUIRED,
|
||||
DISPLAY_MODE_SUBUNIT,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
|
|
@ -214,31 +212,6 @@ def _get_access_token(hass: HomeAssistant, entry: ConfigEntry) -> str:
|
|||
raise ConfigEntryAuthFailed(msg)
|
||||
|
||||
|
||||
def _check_statistics_review_repair(hass: HomeAssistant, entry: TibberPricesConfigEntry) -> None:
|
||||
"""Re-create the statistics-review repair issue on every setup when the flag is set.
|
||||
|
||||
Uses delete + create to guarantee visibility after HA restarts regardless of prior dismissal.
|
||||
We have no way to detect whether the Recorder statistics have actually been fixed, so the
|
||||
flag can only be cleared by an explicit user action: re-saving the currency display settings
|
||||
in the options flow (with the same mode = "I have reviewed everything").
|
||||
Dismissing the HA Repairs notification alone does NOT count as acknowledgement.
|
||||
"""
|
||||
if not entry.data.get(DATA_STATISTICS_REVIEW_REQUIRED):
|
||||
return
|
||||
issue_id = f"currency_display_mode_changed_{entry.entry_id}"
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=False,
|
||||
is_persistent=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="currency_display_mode_changed",
|
||||
translation_placeholders={"home_name": entry.title},
|
||||
)
|
||||
|
||||
|
||||
# https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
|
@ -253,9 +226,6 @@ async def async_setup_entry(
|
|||
# Check for entity migrations (renames, breaking changes) and create repairs
|
||||
check_entity_migrations(hass, entry)
|
||||
|
||||
# Re-create statistics review repair issue fresh (resets any previous dismiss)
|
||||
_check_statistics_review_repair(hass, entry)
|
||||
|
||||
# Preload translations to populate the cache
|
||||
await async_load_translations(hass, "en")
|
||||
await async_load_standard_translations(hass, "en")
|
||||
|
|
|
|||
|
|
@ -25,6 +25,51 @@ if TYPE_CHECKING:
|
|||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
def get_current_phase_type(coordinator_data: dict, *, time: TibberPricesTimeService) -> str | None:
|
||||
"""
|
||||
Return the type of the currently active intra-day price phase.
|
||||
|
||||
Walks today's segments and returns the type ("rising", "falling", or "flat")
|
||||
of the last segment whose start time is ≤ now.
|
||||
|
||||
Args:
|
||||
coordinator_data: The coordinator's data dict.
|
||||
time: TibberPricesTimeService instance.
|
||||
|
||||
Returns:
|
||||
Phase type string or None if no segment data is available.
|
||||
|
||||
"""
|
||||
if not coordinator_data:
|
||||
return None
|
||||
|
||||
day_patterns = coordinator_data.get("dayPatterns")
|
||||
if not day_patterns:
|
||||
return None
|
||||
|
||||
today_data = day_patterns.get("today")
|
||||
if not today_data:
|
||||
return None
|
||||
|
||||
segments: list[dict] | None = today_data.get("segments")
|
||||
if not segments:
|
||||
return None
|
||||
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
now = time.now()
|
||||
current_type: str | None = None
|
||||
for segment in segments:
|
||||
seg_start_str: str | None = segment.get("start")
|
||||
if not seg_start_str:
|
||||
continue
|
||||
seg_start = parse_datetime(seg_start_str)
|
||||
if seg_start is not None and now >= seg_start:
|
||||
current_type = segment.get("type")
|
||||
|
||||
return current_type
|
||||
|
||||
|
||||
def get_tomorrow_data_available_attributes(
|
||||
coordinator_data: dict,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
|||
from .attributes import (
|
||||
build_async_extra_state_attributes,
|
||||
build_sync_extra_state_attributes,
|
||||
get_current_phase_type,
|
||||
get_price_intervals_attributes,
|
||||
get_tomorrow_data_available_attributes,
|
||||
)
|
||||
|
|
@ -136,6 +137,9 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
|
|||
state_getters = {
|
||||
"peak_price_period": self._peak_price_state,
|
||||
"best_price_period": self._best_price_state,
|
||||
"in_rising_price_phase": lambda: self._in_phase_state("rising"),
|
||||
"in_falling_price_phase": lambda: self._in_phase_state("falling"),
|
||||
"in_flat_price_phase": lambda: self._in_phase_state("flat"),
|
||||
"connection": lambda: get_connection_state(self.coordinator),
|
||||
"tomorrow_data_available": self._tomorrow_data_available_state,
|
||||
"has_ventilation_system": self._has_ventilation_system_state,
|
||||
|
|
@ -182,6 +186,15 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
|
|||
time = self.coordinator.time
|
||||
return time.is_time_in_period(start, end)
|
||||
|
||||
def _in_phase_state(self, phase_type: str) -> bool | None:
|
||||
"""Return True if the current intra-day price phase matches phase_type."""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
current_type = get_current_phase_type(self.coordinator.data, time=self.coordinator.time)
|
||||
if current_type is None:
|
||||
return None
|
||||
return current_type == phase_type
|
||||
|
||||
def _tomorrow_data_available_state(self) -> bool | None:
|
||||
"""Return True if tomorrow's data is fully available, False if not, None if unknown."""
|
||||
# Auth errors: Cannot reliably check - return unknown
|
||||
|
|
|
|||
|
|
@ -19,6 +19,22 @@ ENTITY_DESCRIPTIONS = (
|
|||
translation_key="best_price_period",
|
||||
icon="mdi:clock-check",
|
||||
),
|
||||
# Price phase binary sensors — ON when current intra-day phase matches the type
|
||||
BinarySensorEntityDescription(
|
||||
key="in_rising_price_phase",
|
||||
translation_key="in_rising_price_phase",
|
||||
icon="mdi:trending-up",
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="in_falling_price_phase",
|
||||
translation_key="in_falling_price_phase",
|
||||
icon="mdi:trending-down",
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="in_flat_price_phase",
|
||||
translation_key="in_flat_price_phase",
|
||||
icon="mdi:trending-neutral",
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="connection",
|
||||
translation_key="connection",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ from custom_components.tibber_prices.const import (
|
|||
CONF_VOLATILITY_THRESHOLD_HIGH,
|
||||
CONF_VOLATILITY_THRESHOLD_MODERATE,
|
||||
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||
DATA_STATISTICS_REVIEW_REQUIRED,
|
||||
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
||||
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
||||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||
|
|
@ -512,36 +511,24 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
# async_create_entry automatically handles change detection and listener triggering
|
||||
self._save_options_if_changed()
|
||||
|
||||
# Handle currency display mode change repair + persistent flag
|
||||
# Notify user of currency display mode change via Repairs
|
||||
if old_mode is not None and new_mode is not None and old_mode != new_mode:
|
||||
issue_id = f"currency_display_mode_changed_{self.config_entry.entry_id}"
|
||||
mode_changed = old_mode is not None and new_mode is not None and old_mode != new_mode
|
||||
|
||||
if mode_changed:
|
||||
# Set persistent flag so repair issue reappears after HA restart
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry,
|
||||
data={**self.config_entry.data, DATA_STATISTICS_REVIEW_REQUIRED: True},
|
||||
)
|
||||
# delete + create resets dismissed_version, forcing the issue into view
|
||||
# even if the user had dismissed a previous instance of this issue.
|
||||
# delete + create resets dismissed_version so the issue is always visible
|
||||
# for a new mode change, even if a previous instance was dismissed.
|
||||
ir.async_delete_issue(self.hass, DOMAIN, issue_id)
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=False,
|
||||
is_persistent=True,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="currency_display_mode_changed",
|
||||
translation_placeholders={
|
||||
"home_name": self.config_entry.title,
|
||||
},
|
||||
)
|
||||
elif self.config_entry.data.get(DATA_STATISTICS_REVIEW_REQUIRED):
|
||||
# User re-saved display settings with same mode = acknowledgement → clear flag
|
||||
new_data = {k: v for k, v in self.config_entry.data.items() if k != DATA_STATISTICS_REVIEW_REQUIRED}
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=new_data)
|
||||
ir.async_delete_issue(self.hass, DOMAIN, issue_id)
|
||||
|
||||
# Return to menu for more changes
|
||||
return await self.async_step_init()
|
||||
|
|
|
|||
|
|
@ -25,10 +25,6 @@ DATA_CHART_CONFIG = "chart_config" # Key for chart export config in hass.data
|
|||
DATA_CHART_METADATA_CONFIG = "chart_metadata_config" # Key for chart metadata config in hass.data
|
||||
|
||||
# Config entry data flag: set when user switches currency display mode.
|
||||
# Triggers a fresh (un-dismissed) repair issue on every setup/reload until
|
||||
# the user explicitly re-saves the currency settings to acknowledge.
|
||||
DATA_STATISTICS_REVIEW_REQUIRED = "statistics_review_required"
|
||||
|
||||
# Configuration keys
|
||||
CONF_EXTENDED_DESCRIPTIONS = "extended_descriptions"
|
||||
CONF_VIRTUAL_TIME_OFFSET_DAYS = (
|
||||
|
|
|
|||
|
|
@ -88,11 +88,24 @@ TIME_SENSITIVE_ENTITY_KEYS = frozenset(
|
|||
# Binary sensors that check if current time is in a period
|
||||
"peak_price_period",
|
||||
"best_price_period",
|
||||
# Binary sensors for current intra-day price phase
|
||||
"in_rising_price_phase",
|
||||
"in_falling_price_phase",
|
||||
"in_flat_price_phase",
|
||||
# Best/Peak price timestamp sensors (periods only change at interval boundaries)
|
||||
"best_price_end_time",
|
||||
"best_price_next_start_time",
|
||||
"peak_price_end_time",
|
||||
"peak_price_next_start_time",
|
||||
# Current price phase timing sensors (phase boundaries only change at interval boundaries)
|
||||
"current_price_phase_end_time",
|
||||
"current_price_phase_duration",
|
||||
"next_rising_phase_start_time",
|
||||
"next_falling_phase_start_time",
|
||||
"next_flat_phase_start_time",
|
||||
# Current/next price phase enum sensors
|
||||
"current_price_phase",
|
||||
"next_price_phase",
|
||||
# Price rank sensors (rank of current/next/previous interval within a day scope)
|
||||
"current_interval_price_rank_today",
|
||||
"current_interval_price_rank_tomorrow",
|
||||
|
|
@ -128,6 +141,13 @@ MINUTE_UPDATE_ENTITY_KEYS = frozenset(
|
|||
"peak_price_remaining_minutes",
|
||||
"peak_price_progress",
|
||||
"peak_price_next_in_minutes",
|
||||
# Current price phase countdown/progress sensors (need minute updates)
|
||||
"current_price_phase_remaining_minutes",
|
||||
"current_price_phase_progress",
|
||||
# Next-phase countdown sensors (need minute updates)
|
||||
"next_rising_phase_in_minutes",
|
||||
"next_falling_phase_in_minutes",
|
||||
"next_flat_phase_in_minutes",
|
||||
# Trend change countdown sensor (needs minute updates)
|
||||
"next_price_trend_change_in",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -501,6 +501,66 @@
|
|||
"long_description": "Klassifiziert morgen (sobald Daten verfügbar sind, typisch nach 13 Uhr) in ein Preismuster mit demselben Algorithmus wie heute. Die Attribute valley_start/valley_end oder peak_start/peak_end geben Knickpunktzeiten für das primäre Extremum an.",
|
||||
"usage_tips": "Richte Abendautomationen ein, die das morgige Muster lesen und Wärmepumpe, Autolader oder Warmwasserbereiter für den nächsten Tag vorkonfigurieren. Kombiniere mit dem tomorrow_data_available Binärsensor."
|
||||
},
|
||||
"current_price_phase": {
|
||||
"description": "Ob die Strompreise aktuell steigen, fallen oder stabil sind – innerhalb der tageszeitlichen Preisform",
|
||||
"long_description": "Zeigt die Preisbewegungsrichtung zum aktuellen Zeitpunkt, indem das aktive monotone Segment der heutigen Preiskurve ermittelt wird. Der Tagesverlauf wird in aufeinanderfolgende steigende, fallende oder flache Abschnitte (Phasen) unterteilt. Dieser Sensor zeigt, in welcher Phase du dich gerade befindest. Attribute: Startzeit und Endzeit der Phase, Preisspanne (min/max/mean), Position im Tagesverlauf (segment_index und segment_count) sowie die vollständige Liste aller heutigen Phasen (all_segments). Aktualisierung alle 15 Minuten.",
|
||||
"usage_tips": "Nutzen in Automationen: 'Wenn current_price_phase = fallend, flexible Lasten verschieben, bis der Preis seinen Tiefpunkt erreicht hat'. Kombiniere mit dem Sensor Heutiges Preismuster, um sowohl die Gesamtform des Tages als auch deine aktuelle Position darin zu sehen. Prüfe segment_index und segment_count: z. B. segment_index=0 und Phase=fallend bedeutet, die Preise fallen seit Mitternacht. Nutze all_segments in Templates oder Dashboards, um den vollständigen Tagesverlauf anzuzeigen."
|
||||
},
|
||||
"next_price_phase": {
|
||||
"description": "Die nächste tageszeitliche Preisphase – was nach der aktuellen Preisbewegung kommt",
|
||||
"long_description": "Zeigt die monotone Preisphase, die nach der aktuell aktiven Phase folgt. Das Attribut start zeigt genau, wann die nächste Phase beginnt – ideal für zeitgenaue Automationen. Wenn die aktuelle Phase die letzte des Tages ist (z.B. der abendliche Abwärtstrend), wird dieser Sensor nicht verfügbar. Attribute: start (wann sie beginnt), end, Preisspanne (min/max/mean), segment_index, segment_count. Aktualisierung alle 15 Minuten.",
|
||||
"usage_tips": "Nutzen in Automationen: 'Wenn next_price_phase = steigend und next_price_phase.start in weniger als 1 Stunde, Waschmaschine jetzt starten'. Oder kombiniere mit current_price_phase: 'Wenn current_price_phase = fallend und next_price_phase = flach, nähern wir uns dem Tagestiefpunkt – guter Zeitpunkt für flexible Lasten'. Das Attribut start ist besonders wertvoll: Automationen können exakt dann ausgelöst werden, wenn die nächste Phase beginnt."
|
||||
},
|
||||
"current_price_phase_end_time": {
|
||||
"description": "When the current intra-day price phase ends",
|
||||
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase.",
|
||||
"usage_tips": "Use in automations to schedule tasks that must finish before prices change."
|
||||
},
|
||||
"current_price_phase_remaining_minutes": {
|
||||
"description": "Minutes remaining in the current price phase",
|
||||
"long_description": "Shows how many minutes are left in the current intra-day price phase. Updates every minute.",
|
||||
"usage_tips": "Use in automations: 'If current_price_phase = falling and remaining < 30, start the dishwasher now'."
|
||||
},
|
||||
"current_price_phase_duration": {
|
||||
"description": "Total duration of the current price phase",
|
||||
"long_description": "Shows the total length of the currently active price phase in hours.",
|
||||
"usage_tips": "Combine with remaining minutes to understand how far through the phase you are."
|
||||
},
|
||||
"current_price_phase_progress": {
|
||||
"description": "How far through the current price phase we are",
|
||||
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0–100%). Updates every minute.",
|
||||
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase."
|
||||
},
|
||||
"next_rising_phase_start_time": {
|
||||
"description": "When the next rising price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use to schedule loads before prices start rising again."
|
||||
},
|
||||
"next_falling_phase_start_time": {
|
||||
"description": "When the next falling price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use to delay flexible loads until the next price drop starts."
|
||||
},
|
||||
"next_flat_phase_start_time": {
|
||||
"description": "When the next flat (stable) price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming flat price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use for scheduling loads that need predictable costs over time."
|
||||
},
|
||||
"next_rising_phase_in_minutes": {
|
||||
"description": "Minutes until the next rising price phase begins",
|
||||
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use in countdown automations to alert before the next price rise."
|
||||
},
|
||||
"next_falling_phase_in_minutes": {
|
||||
"description": "Minutes until the next falling price phase begins",
|
||||
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use to time flexible loads: delay until the upcoming price drop."
|
||||
},
|
||||
"next_flat_phase_in_minutes": {
|
||||
"description": "Minutes until the next flat (stable) price phase begins",
|
||||
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use to anticipate price stabilisation after a volatile phase."
|
||||
},
|
||||
"chart_data_export": {
|
||||
"description": "Datenexport für Dashboard-Integrationen",
|
||||
"long_description": "Dieser Sensor ruft den get_chartdata-Service mit deiner konfigurierten YAML-Konfiguration auf und stellt das Ergebnis als Entity-Attribute bereit. Der Status zeigt 'ready' wenn Daten verfügbar sind, 'error' bei Fehlern, oder 'pending' vor dem ersten Aufruf. Perfekt für Dashboard-Integrationen wie ApexCharts, die Preisdaten aus Entity-Attributen lesen.",
|
||||
|
|
@ -583,6 +643,21 @@
|
|||
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den unteren 20% der heutigen Preise liegt",
|
||||
"usage_tips": "Nutze dies, um Geräte mit hohem Verbrauch während der günstigsten Intervalle zu betreiben"
|
||||
},
|
||||
"in_rising_price_phase": {
|
||||
"description": "Whether prices are currently in a rising phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is rising.",
|
||||
"usage_tips": "Use in automations to delay or avoid running flexible loads during rising prices."
|
||||
},
|
||||
"in_falling_price_phase": {
|
||||
"description": "Whether prices are currently in a falling phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is falling.",
|
||||
"usage_tips": "Use in automations to take advantage of falling prices for flexible loads."
|
||||
},
|
||||
"in_flat_price_phase": {
|
||||
"description": "Whether prices are currently in a flat (stable) phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is flat and prices are relatively stable.",
|
||||
"usage_tips": "Use for loads that prefer price stability rather than the lowest price."
|
||||
},
|
||||
"connection": {
|
||||
"description": "Ob die Verbindung zur Tibber API funktioniert",
|
||||
"long_description": "Zeigt an, ob die Integration erfolgreich eine Verbindung zur Tibber API herstellen kann",
|
||||
|
|
|
|||
|
|
@ -501,6 +501,66 @@
|
|||
"long_description": "Classifies tomorrow (once data is available, typically after 13:00) into a price shape using the same algorithm as today. The valley_start / valley_end or peak_start / peak_end attributes give knee-point times for the primary extremum so you can pre-schedule loads the evening before.",
|
||||
"usage_tips": "Set up evening automations that read tomorrow's pattern and pre-configure heat pump schedules, car charging timers, or water heater settings for the following day. Pair with the tomorrow_data_available binary sensor to trigger the automation only when data is ready."
|
||||
},
|
||||
"current_price_phase": {
|
||||
"description": "Whether electricity prices are currently rising, falling, or flat within today's intra-day price shape",
|
||||
"long_description": "Shows the direction of price movement at the current time by identifying which monotone segment of today's price curve you are in. Today's prices are split into consecutively rising, falling, or flat stretches (phases). This sensor tells you which phase is active right now. Attributes include the phase's start and end times, its price range (min/max/mean), its position among all phases of the day (segment_index and segment_count), and the full list of all today's phases (all_segments). Updates every 15 minutes.",
|
||||
"usage_tips": "Use in automations: 'If current_price_phase = falling, delay flexible loads until prices bottom out'. Pair with the Today's Price Pattern sensor to see both the overall shape of the day and your current position in it. Check segment_index and segment_count to understand how far through the intra-day movement you are — e.g. if segment_index=0 and the phase is already falling, prices have been declining since midnight. Use all_segments in templates or dashboards to display the full day ahead."
|
||||
},
|
||||
"next_price_phase": {
|
||||
"description": "The next intra-day price phase — what comes after the current price movement",
|
||||
"long_description": "Shows the monotone price phase that will follow after the currently active phase ends. The start attribute tells you exactly when the next phase begins, making it easy to schedule automations. When the current phase is the last one of the day (e.g. the final evening fall), this sensor becomes unavailable. Attributes: start (when it begins), end, price range (min/max/mean), segment_index, segment_count. Updates every 15 minutes.",
|
||||
"usage_tips": "Use in automations: 'If next_price_phase = rising and next_price_phase.start is within 1 hour, start the washing machine now'. Or combine with current_price_phase: 'If current_price_phase = falling and next_price_phase = flat, we are approaching the daily low — good time for flexible loads'. The start attribute is particularly valuable: trigger automations precisely when the next phase begins."
|
||||
},
|
||||
"current_price_phase_end_time": {
|
||||
"description": "When the current intra-day price phase ends",
|
||||
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase. Becomes unavailable when no segment data is available. Updates at interval boundaries (every 15 minutes). Pair with current_price_phase to know both what phase you are in and when it ends.",
|
||||
"usage_tips": "Use in automations to schedule tasks that must finish before prices change: 'Start washing machine if current_price_phase = falling and current_price_phase_end_time is more than 2 hours away'. Or to alert when a cheap phase is about to end."
|
||||
},
|
||||
"current_price_phase_remaining_minutes": {
|
||||
"description": "Minutes remaining in the current price phase",
|
||||
"long_description": "Shows how many minutes are left in the current intra-day price phase (rising, falling, or flat). Updates every minute for countdown precision. Returns 0 when no segment data is available. The remaining_minutes attribute mirrors the sensor value in integer minutes for simpler automation templates.",
|
||||
"usage_tips": "Use in automations: 'If current_price_phase = falling and current_price_phase_remaining_minutes < 30, start the dishwasher now before prices level out'. Also useful for dashboard cards showing a countdown bar until the phase transition."
|
||||
},
|
||||
"current_price_phase_duration": {
|
||||
"description": "Total duration of the current price phase",
|
||||
"long_description": "Shows the total length of the currently active price phase in hours (expressed in minutes internally). Updates at interval boundaries. This tells you how long the current trend segment lasts in total — useful for understanding whether it is a brief fluctuation or an extended period of rising or falling prices.",
|
||||
"usage_tips": "Combine with current_price_phase_remaining_minutes to estimate how far through the phase you are, or compare with current_price_phase_progress to understand the time profile of the current trend."
|
||||
},
|
||||
"current_price_phase_progress": {
|
||||
"description": "How far through the current price phase we are",
|
||||
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0–100%). Updates every minute. A value near 0% means the phase just started; near 100% means it is about to end. Returns 0 when no segment data is available.",
|
||||
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase. In automations: 'If current_price_phase = falling and current_price_phase_progress > 80, the cheapest prices are close — prepare flexible loads now'."
|
||||
},
|
||||
"next_rising_phase_start_time": {
|
||||
"description": "When the next rising price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases. Returns unavailable when no more rising phases exist in today's or tomorrow's data. Updates at interval boundaries (every 15 minutes).",
|
||||
"usage_tips": "Use with in_falling_price_phase and next_rising_phase_start_time to schedule flexible loads: run them now while prices fall, and wrap up before the next rise starts. 'If in_falling_price_phase is ON and next_rising_phase_start_time is less than 1 hour away, start the washing machine immediately'."
|
||||
},
|
||||
"next_falling_phase_start_time": {
|
||||
"description": "When the next falling price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases. Returns unavailable when no more falling phases exist in today's or tomorrow's data. Updates at interval boundaries (every 15 minutes).",
|
||||
"usage_tips": "Use to delay flexible loads until the next price drop starts. 'If next_falling_phase_start_time is within 2 hours, consider waiting before starting the dishwasher or heat pump'."
|
||||
},
|
||||
"next_flat_phase_start_time": {
|
||||
"description": "When the next flat (stable) price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming flat price segment (where prices show little variation) across today's remaining phases and tomorrow's phases. Returns unavailable when no more flat phases exist in the available data. Updates at interval boundaries (every 15 minutes).",
|
||||
"usage_tips": "Flat phases indicate price stability — useful for scheduling loads that need a predictable cost over time. 'If next_flat_phase_start_time is within 30 minutes and current_price_phase = rising, the price peak is nearly over'."
|
||||
},
|
||||
"next_rising_phase_in_minutes": {
|
||||
"description": "Minutes until the next rising price phase begins",
|
||||
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute. Returns unavailable if no more rising phases exist in today's or tomorrow's data. The next_in_minutes attribute mirrors the sensor value in integer minutes for automation templates.",
|
||||
"usage_tips": "Use in countdown automations: 'Alert me 15 minutes before the next price rise so I can delay flexible loads'. Combine with in_falling_price_phase: if currently falling and a rise is imminent, act before prices start climbing."
|
||||
},
|
||||
"next_falling_phase_in_minutes": {
|
||||
"description": "Minutes until the next falling price phase begins",
|
||||
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute. Returns unavailable if no more falling phases exist in today's or tomorrow's data. The next_in_minutes attribute mirrors the sensor value in integer minutes for automation templates.",
|
||||
"usage_tips": "Use to time flexible loads optimally: 'If next_falling_phase_in_minutes < 60, delay the washing machine start to benefit from the upcoming price drop'."
|
||||
},
|
||||
"next_flat_phase_in_minutes": {
|
||||
"description": "Minutes until the next flat (stable) price phase begins",
|
||||
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute. Returns unavailable if no more flat phases exist in today's or tomorrow's data. The next_in_minutes attribute mirrors the sensor value in integer minutes.",
|
||||
"usage_tips": "Use to anticipate price stabilisation after a volatile phase. 'If next_flat_phase_in_minutes < 30 and current_price_phase = rising, the price peak will soon level off — consider delaying loads until then'."
|
||||
},
|
||||
"chart_data_export": {
|
||||
"description": "Data export for dashboard integrations",
|
||||
"long_description": "This binary sensor calls the get_chartdata service with your configured YAML parameters and exposes the result as entity attributes. The state is 'on' when the service call succeeds and data is available, 'off' when the call fails or no configuration is set. Perfect for dashboard integrations like ApexCharts that need to read price data from entity attributes.",
|
||||
|
|
@ -583,6 +643,21 @@
|
|||
"long_description": "Turns on when the current price is in the bottom 20% of today's prices",
|
||||
"usage_tips": "Use this to run high-consumption appliances during the cheapest intervals"
|
||||
},
|
||||
"in_rising_price_phase": {
|
||||
"description": "Whether prices are currently in a rising phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is rising — i.e. prices have been moving upward since the last phase transition. Exactly one of in_rising_price_phase, in_falling_price_phase, and in_flat_price_phase is ON at any time. Becomes unavailable when no segment data is available.",
|
||||
"usage_tips": "Use in automations to delay or avoid running flexible loads: 'If in_rising_price_phase is ON, postpone the dishwasher'. Pair with next_falling_phase_start_time to know when prices will start dropping again."
|
||||
},
|
||||
"in_falling_price_phase": {
|
||||
"description": "Whether prices are currently in a falling phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is falling — i.e. prices have been dropping since the last phase transition. This is often a good window to start flexible loads. Exactly one of in_rising_price_phase, in_falling_price_phase, and in_flat_price_phase is ON at any time.",
|
||||
"usage_tips": "Use in automations to take advantage of falling prices: 'If in_falling_price_phase is ON and current_price_phase_remaining_minutes > 60, start the washing machine'. Combine with next_rising_phase_start_time to avoid starting a load that won't finish before prices rise."
|
||||
},
|
||||
"in_flat_price_phase": {
|
||||
"description": "Whether prices are currently in a flat (stable) phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is flat — i.e. prices are relatively stable with no significant rise or fall. Flat phases indicate predictable costs, making them suitable for loads with uncertain or variable duration. Exactly one of in_rising_price_phase, in_falling_price_phase, and in_flat_price_phase is ON at any time.",
|
||||
"usage_tips": "Use for loads that are indifferent to price direction but prefer stability: heat pumps in steady-state mode, background charging, or water heater top-ups. Combine with current_price_phase_end_time to know how long the stable window lasts."
|
||||
},
|
||||
"connection": {
|
||||
"description": "Whether the connection to the Tibber API is working",
|
||||
"long_description": "Indicates if the integration can successfully connect to the Tibber API",
|
||||
|
|
|
|||
|
|
@ -501,6 +501,66 @@
|
|||
"long_description": "Klassifiserer i morgen (når data er tilgjengelig, typisk etter kl. 13) i et prismønster med samme algoritme som i dag. Attributtene valley_start/valley_end eller peak_start/peak_end gir knepunktstider.",
|
||||
"usage_tips": "Sett opp kveldsautomasjonar som leser morgendagens mønster og forhåndskonfigurerer varmepumpe, billader eller varmtvannsberedere. Kombiner med tomorrow_data_available-binærsensoren."
|
||||
},
|
||||
"current_price_phase": {
|
||||
"description": "Om strømprisene nå stiger, faller eller er stabile – innenfor dagens intra-dag prisform",
|
||||
"long_description": "Viser retningen på prisbevegelsen nå ved å identifisere det aktive monotone segmentet i dagens priskurve. Dagens priser deles inn i fortløpende stigende, fallende eller flate strekninger (faser). Denne sensoren viser hvilken fase som er aktivt akkurat nå. Attributter inkluderer fasens start- og sluttid, prisområde (min/maks/gjennomsnitt), posisjon blant dagens faser (segment_index og segment_count) og den fullstendige listen over alle dagens faser (all_segments). Oppdateres hvert 15. minutt.",
|
||||
"usage_tips": "Bruk i automasjonar: 'Hvis current_price_phase = fallende, vent med fleksible laster til prisene når bunnen'. Kombiner med Dagens Prismønster for å se både den overordnede dagformen og din nåværende posisjon i den. Sjekk segment_index og segment_count for å forstå hvor langt inn i bevegelsen du er. Bruk all_segments i maler eller dashbord for å vise hele dagsforløpet."
|
||||
},
|
||||
"next_price_phase": {
|
||||
"description": "Den neste intra-dag prisfasen – hva som kommer etter den nåværende prisbevegelsen",
|
||||
"long_description": "Viser det monotone prissegmentet som følger etter den for øyeblikket aktive fasen. Attributtet start viser nøyaktig når neste fase begynner, noe som gjør det enkelt å planlegge automasjonar. Når den nåværende fasen er den siste for dagen (f.eks. det siste kveldsfall), blir denne sensoren utilgjengelig. Attributter: start (når den begynner), end, prisområde (min/maks/gjennomsnitt), segment_index, segment_count. Oppdateres hvert 15. minutt.",
|
||||
"usage_tips": "Bruk i automasjonar: 'Hvis next_price_phase = stigende og next_price_phase.start er innen 1 time, start vaskemaskin nå'. Eller kombiner med current_price_phase: 'Hvis current_price_phase = fallende og next_price_phase = flat, nærmer vi oss daglig lavpunkt – godt tidspunkt for fleksible laster'. Attributtet start er spesielt verdifullt: utløs automasjonar nøyaktig når neste fase begynner."
|
||||
},
|
||||
"current_price_phase_end_time": {
|
||||
"description": "When the current intra-day price phase ends",
|
||||
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase.",
|
||||
"usage_tips": "Use in automations to schedule tasks that must finish before prices change."
|
||||
},
|
||||
"current_price_phase_remaining_minutes": {
|
||||
"description": "Minutes remaining in the current price phase",
|
||||
"long_description": "Shows how many minutes are left in the current intra-day price phase. Updates every minute.",
|
||||
"usage_tips": "Use in automations: 'If current_price_phase = falling and remaining < 30, start the dishwasher now'."
|
||||
},
|
||||
"current_price_phase_duration": {
|
||||
"description": "Total duration of the current price phase",
|
||||
"long_description": "Shows the total length of the currently active price phase in hours.",
|
||||
"usage_tips": "Combine with remaining minutes to understand how far through the phase you are."
|
||||
},
|
||||
"current_price_phase_progress": {
|
||||
"description": "How far through the current price phase we are",
|
||||
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0–100%). Updates every minute.",
|
||||
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase."
|
||||
},
|
||||
"next_rising_phase_start_time": {
|
||||
"description": "When the next rising price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use to schedule loads before prices start rising again."
|
||||
},
|
||||
"next_falling_phase_start_time": {
|
||||
"description": "When the next falling price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use to delay flexible loads until the next price drop starts."
|
||||
},
|
||||
"next_flat_phase_start_time": {
|
||||
"description": "When the next flat (stable) price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming flat price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use for scheduling loads that need predictable costs over time."
|
||||
},
|
||||
"next_rising_phase_in_minutes": {
|
||||
"description": "Minutes until the next rising price phase begins",
|
||||
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use in countdown automations to alert before the next price rise."
|
||||
},
|
||||
"next_falling_phase_in_minutes": {
|
||||
"description": "Minutes until the next falling price phase begins",
|
||||
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use to time flexible loads: delay until the upcoming price drop."
|
||||
},
|
||||
"next_flat_phase_in_minutes": {
|
||||
"description": "Minutes until the next flat (stable) price phase begins",
|
||||
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use to anticipate price stabilisation after a volatile phase."
|
||||
},
|
||||
"chart_data_export": {
|
||||
"description": "Dataeksport for dashboardintegrasjoner",
|
||||
"long_description": "Denne sensoren kaller get_chartdata-tjenesten med din konfigurerte YAML-konfigurasjon og eksponerer resultatet som entitetsattributter. Status viser 'ready' når data er tilgjengelig, 'error' ved feil, eller 'pending' før første kall. Perfekt for dashboardintegrasjoner som ApexCharts som trenger å lese prisdata fra entitetsattributter.",
|
||||
|
|
@ -583,6 +643,21 @@
|
|||
"long_description": "Slår seg på når nåværende pris er i bunn 20% av dagens priser",
|
||||
"usage_tips": "Bruk dette til å kjøre høyforbruksapparater i de billigste intervallene"
|
||||
},
|
||||
"in_rising_price_phase": {
|
||||
"description": "Whether prices are currently in a rising phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is rising.",
|
||||
"usage_tips": "Use in automations to delay or avoid running flexible loads during rising prices."
|
||||
},
|
||||
"in_falling_price_phase": {
|
||||
"description": "Whether prices are currently in a falling phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is falling.",
|
||||
"usage_tips": "Use in automations to take advantage of falling prices for flexible loads."
|
||||
},
|
||||
"in_flat_price_phase": {
|
||||
"description": "Whether prices are currently in a flat (stable) phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is flat and prices are relatively stable.",
|
||||
"usage_tips": "Use for loads that prefer price stability rather than the lowest price."
|
||||
},
|
||||
"connection": {
|
||||
"description": "Om tilkoblingen til Tibber API fungerer",
|
||||
"long_description": "Indikerer om integrasjonen kan koble til Tibber API",
|
||||
|
|
|
|||
|
|
@ -501,6 +501,66 @@
|
|||
"long_description": "Classificeert morgen (zodra data beschikbaar is, doorgaans na 13:00) in een prijspatroon met hetzelfde algoritme als vandaag. De attributen valley_start/valley_end of peak_start/peak_end geven kniepunttijden voor het primaire extremum.",
|
||||
"usage_tips": "Stel avondautomations in die het patroon van morgen lezen en warmtepomp, autolader of boiler vooraf configureren. Combineer met de tomorrow_data_available binaire sensor."
|
||||
},
|
||||
"current_price_phase": {
|
||||
"description": "Of elektriciteitsprijzen momenteel stijgen, dalen of stabiel zijn – binnen de intra-dag prijsvorm van vandaag",
|
||||
"long_description": "Toont de richting van de prijsbeweging op dit moment door het actieve monotone segment van de prijscurve van vandaag te bepalen. De dagprijzen worden opgesplitst in opeenvolgende stijgende, dalende of vlakke stukken (fasen). Deze sensor toont in welke fase je je nu bevindt. Attributen zijn onder andere de start- en eindtijd van de fase, het prijsbereik (min/max/gemiddelde), de positie binnen de dagelijkse fasen (segment_index en segment_count) en de volledige lijst van alle fasen van vandaag (all_segments). Elke 15 minuten bijgewerkt.",
|
||||
"usage_tips": "Gebruik in automations: 'Als current_price_phase = dalend, wacht met flexibele lasten totdat de prijzen de bodem bereiken'. Combineer met de Prijspatroon Vandaag-sensor om zowel de algemene dagvorm als je huidige positie daarin te zien. Controleer segment_index en segment_count om te begrijpen hoe ver je bent in de intra-dag beweging. Gebruik all_segments in sjablonen of dashboards om het volledige dagverloop te tonen."
|
||||
},
|
||||
"next_price_phase": {
|
||||
"description": "De volgende intra-dag prijsfase – wat er na de huidige prijsbeweging komt",
|
||||
"long_description": "Toont het monotone prijssegment dat volgt na de momenteel actieve fase. Het attribuut start geeft precies aan wanneer de volgende fase begint, wat het eenvoudig maakt om automations nauwkeurig te plannen. Wanneer de huidige fase de laatste van de dag is (bijv. de laatste avonddaling), wordt deze sensor niet beschikbaar. Attributen: start (wanneer het begint), end, prijsbereik (min/max/gemiddelde), segment_index, segment_count. Elke 15 minuten bijgewerkt.",
|
||||
"usage_tips": "Gebruik in automations: 'Als next_price_phase = stijgend en next_price_phase.start binnen 1 uur is, start de wasmachine nu'. Of combineer met current_price_phase: 'Als current_price_phase = dalend en next_price_phase = vlak, naderen we het dagdieptepunt – goed moment voor flexibele lasten'. Het attribuut start is bijzonder waardevol: activeer automations precies wanneer de volgende fase begint."
|
||||
},
|
||||
"current_price_phase_end_time": {
|
||||
"description": "When the current intra-day price phase ends",
|
||||
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase.",
|
||||
"usage_tips": "Use in automations to schedule tasks that must finish before prices change."
|
||||
},
|
||||
"current_price_phase_remaining_minutes": {
|
||||
"description": "Minutes remaining in the current price phase",
|
||||
"long_description": "Shows how many minutes are left in the current intra-day price phase. Updates every minute.",
|
||||
"usage_tips": "Use in automations: 'If current_price_phase = falling and remaining < 30, start the dishwasher now'."
|
||||
},
|
||||
"current_price_phase_duration": {
|
||||
"description": "Total duration of the current price phase",
|
||||
"long_description": "Shows the total length of the currently active price phase in hours.",
|
||||
"usage_tips": "Combine with remaining minutes to understand how far through the phase you are."
|
||||
},
|
||||
"current_price_phase_progress": {
|
||||
"description": "How far through the current price phase we are",
|
||||
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0–100%). Updates every minute.",
|
||||
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase."
|
||||
},
|
||||
"next_rising_phase_start_time": {
|
||||
"description": "When the next rising price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use to schedule loads before prices start rising again."
|
||||
},
|
||||
"next_falling_phase_start_time": {
|
||||
"description": "When the next falling price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use to delay flexible loads until the next price drop starts."
|
||||
},
|
||||
"next_flat_phase_start_time": {
|
||||
"description": "When the next flat (stable) price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming flat price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use for scheduling loads that need predictable costs over time."
|
||||
},
|
||||
"next_rising_phase_in_minutes": {
|
||||
"description": "Minutes until the next rising price phase begins",
|
||||
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use in countdown automations to alert before the next price rise."
|
||||
},
|
||||
"next_falling_phase_in_minutes": {
|
||||
"description": "Minutes until the next falling price phase begins",
|
||||
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use to time flexible loads: delay until the upcoming price drop."
|
||||
},
|
||||
"next_flat_phase_in_minutes": {
|
||||
"description": "Minutes until the next flat (stable) price phase begins",
|
||||
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use to anticipate price stabilisation after a volatile phase."
|
||||
},
|
||||
"chart_data_export": {
|
||||
"description": "Data-export voor dashboard-integraties",
|
||||
"long_description": "Deze sensor roept de get_chartdata-service aan met jouw geconfigureerde YAML-configuratie en stelt het resultaat beschikbaar als entiteitsattributen. De status toont 'ready' wanneer data beschikbaar is, 'error' bij fouten, of 'pending' voor de eerste aanroep. Perfekt voor dashboard-integraties zoals ApexCharts die prijsgegevens uit entiteitsattributen moeten lezen.",
|
||||
|
|
@ -583,6 +643,21 @@
|
|||
"long_description": "Wordt geactiveerd wanneer de huidige prijs in de onderste 20% van de prijzen van vandaag ligt",
|
||||
"usage_tips": "Gebruik dit om apparaten met hoog verbruik te laten draaien tijdens de goedkoopste intervallen"
|
||||
},
|
||||
"in_rising_price_phase": {
|
||||
"description": "Whether prices are currently in a rising phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is rising.",
|
||||
"usage_tips": "Use in automations to delay or avoid running flexible loads during rising prices."
|
||||
},
|
||||
"in_falling_price_phase": {
|
||||
"description": "Whether prices are currently in a falling phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is falling.",
|
||||
"usage_tips": "Use in automations to take advantage of falling prices for flexible loads."
|
||||
},
|
||||
"in_flat_price_phase": {
|
||||
"description": "Whether prices are currently in a flat (stable) phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is flat and prices are relatively stable.",
|
||||
"usage_tips": "Use for loads that prefer price stability rather than the lowest price."
|
||||
},
|
||||
"connection": {
|
||||
"description": "Of de verbinding met de Tibber API werkt",
|
||||
"long_description": "Geeft aan of de integratie succesvol verbinding kan maken met de Tibber API",
|
||||
|
|
|
|||
|
|
@ -501,6 +501,66 @@
|
|||
"long_description": "Klassificerar imorgon (när data finns tillgänglig, vanligtvis efter 13:00) i ett prismönster med samma algoritm som idag. Attributen valley_start/valley_end eller peak_start/peak_end ger knäpunkttider för det primära extremvärdet.",
|
||||
"usage_tips": "Ställ in kvällsautomationer som läser morgondagens mönster och förkonfigurerar värmepump, billaddare eller varmvattenberedare. Kombinera med tomorrow_data_available-binärsensorn."
|
||||
},
|
||||
"current_price_phase": {
|
||||
"description": "Om elpriserna för närvarande stiger, faller eller är stabila – inom dagens intra-dag prisform",
|
||||
"long_description": "Visar riktningen på priserörelsen just nu genom att identifiera det aktiva monotona segmentet i dagens priskurva. Dagens priser delas upp i på varandra följande stigande, fallande eller flata sträckor (faser). Denna sensor visar vilken fas som är aktiv just nu. Attribut inkluderar fasens start- och sluttid, prisintervall (min/max/medel), position bland dagens faser (segment_index och segment_count) samt den fullständiga listan över alla dagens faser (all_segments). Uppdateras var 15:e minut.",
|
||||
"usage_tips": "Använd i automationer: 'Om current_price_phase = fallande, vänta med flexibla laster tills priserna når botten'. Kombinera med Dagens Prismönster för att se både den övergripande dagformen och din aktuella position i den. Kontrollera segment_index och segment_count för att förstå hur långt in i rörelsen du är. Använd all_segments i mallar eller dashboards för att visa hela dagsförloppet."
|
||||
},
|
||||
"next_price_phase": {
|
||||
"description": "Nästa intra-dag prisfas – vad som kommer efter den aktuella prisbevegelsen",
|
||||
"long_description": "Visar det monotona prissegment som följer efter den för närvarande aktiva fasen. Attributet start visar exakt när nästa fas börjar, vilket gör det enkelt att schemalägga automationer. När den aktuella fasen är den sista för dagen (t.ex. den sista kvällsminskningen), blir denna sensor otillgänglig. Attribut: start (när den börjar), end, prisintervall (min/max/medel), segment_index, segment_count. Uppdateras var 15:e minut.",
|
||||
"usage_tips": "Använd i automationer: 'Om next_price_phase = stigande och next_price_phase.start är inom 1 timme, starta tvättmaskinen nu'. Eller kombinera med current_price_phase: 'Om current_price_phase = fallande och next_price_phase = flat, närmar vi oss dagens lågpunkt – bra tid för flexibla laster'. Attributet start är särskilt värdefull: utlös automationer precis när nästa fas börjar."
|
||||
},
|
||||
"current_price_phase_end_time": {
|
||||
"description": "When the current intra-day price phase ends",
|
||||
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase.",
|
||||
"usage_tips": "Use in automations to schedule tasks that must finish before prices change."
|
||||
},
|
||||
"current_price_phase_remaining_minutes": {
|
||||
"description": "Minutes remaining in the current price phase",
|
||||
"long_description": "Shows how many minutes are left in the current intra-day price phase. Updates every minute.",
|
||||
"usage_tips": "Use in automations: 'If current_price_phase = falling and remaining < 30, start the dishwasher now'."
|
||||
},
|
||||
"current_price_phase_duration": {
|
||||
"description": "Total duration of the current price phase",
|
||||
"long_description": "Shows the total length of the currently active price phase in hours.",
|
||||
"usage_tips": "Combine with remaining minutes to understand how far through the phase you are."
|
||||
},
|
||||
"current_price_phase_progress": {
|
||||
"description": "How far through the current price phase we are",
|
||||
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0–100%). Updates every minute.",
|
||||
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase."
|
||||
},
|
||||
"next_rising_phase_start_time": {
|
||||
"description": "When the next rising price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use to schedule loads before prices start rising again."
|
||||
},
|
||||
"next_falling_phase_start_time": {
|
||||
"description": "When the next falling price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use to delay flexible loads until the next price drop starts."
|
||||
},
|
||||
"next_flat_phase_start_time": {
|
||||
"description": "When the next flat (stable) price phase begins",
|
||||
"long_description": "Shows the timestamp of the next upcoming flat price segment across today's remaining phases and tomorrow's phases.",
|
||||
"usage_tips": "Use for scheduling loads that need predictable costs over time."
|
||||
},
|
||||
"next_rising_phase_in_minutes": {
|
||||
"description": "Minutes until the next rising price phase begins",
|
||||
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use in countdown automations to alert before the next price rise."
|
||||
},
|
||||
"next_falling_phase_in_minutes": {
|
||||
"description": "Minutes until the next falling price phase begins",
|
||||
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use to time flexible loads: delay until the upcoming price drop."
|
||||
},
|
||||
"next_flat_phase_in_minutes": {
|
||||
"description": "Minutes until the next flat (stable) price phase begins",
|
||||
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute.",
|
||||
"usage_tips": "Use to anticipate price stabilisation after a volatile phase."
|
||||
},
|
||||
"chart_data_export": {
|
||||
"description": "Dataexport för dashboard-integrationer",
|
||||
"long_description": "Denna sensor anropar get_chartdata-tjänsten med din konfigurerade YAML-konfiguration och exponerar resultatet som entitetsattribut. Statusen visar 'ready' när data är tillgänglig, 'error' vid fel, eller 'pending' före första anropet. Perfekt för dashboard-integrationer som ApexCharts som behöver läsa prisdata från entitetsattribut.",
|
||||
|
|
@ -583,6 +643,21 @@
|
|||
"long_description": "Aktiveras när nuvarande pris ligger i botten 20% av dagens priser",
|
||||
"usage_tips": "Använd detta för att köra högkonsumtionsapparater under de billigaste intervallerna"
|
||||
},
|
||||
"in_rising_price_phase": {
|
||||
"description": "Whether prices are currently in a rising phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is rising.",
|
||||
"usage_tips": "Use in automations to delay or avoid running flexible loads during rising prices."
|
||||
},
|
||||
"in_falling_price_phase": {
|
||||
"description": "Whether prices are currently in a falling phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is falling.",
|
||||
"usage_tips": "Use in automations to take advantage of falling prices for flexible loads."
|
||||
},
|
||||
"in_flat_price_phase": {
|
||||
"description": "Whether prices are currently in a flat (stable) phase",
|
||||
"long_description": "Turns ON when the current intra-day price phase is flat and prices are relatively stable.",
|
||||
"usage_tips": "Use for loads that prefer price stability rather than the lowest price."
|
||||
},
|
||||
"connection": {
|
||||
"description": "Om anslutningen till Tibber API fungerar",
|
||||
"long_description": "Indikerar om integrationen framgångsrikt kan ansluta till Tibber API",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ from .daily_stat import add_statistics_attributes
|
|||
from .future import add_next_avg_attributes, get_future_prices
|
||||
from .interval import add_current_interval_price_attributes
|
||||
from .lifecycle import build_lifecycle_attributes
|
||||
from .metadata import get_day_pattern_attributes
|
||||
from .metadata import get_current_price_phase_attributes, get_day_pattern_attributes, get_next_price_phase_attributes
|
||||
from .timing import _is_timing_or_volatility_sensor
|
||||
from .trend import _add_cached_trend_attributes, _add_timing_or_volatility_attributes
|
||||
from .volatility import add_percentile_rank_attributes, add_volatility_type_attributes, get_prices_for_volatility
|
||||
|
|
@ -195,6 +195,16 @@ def build_sensor_attributes(
|
|||
if day_attrs:
|
||||
attributes.update(day_attrs)
|
||||
|
||||
elif key == "current_price_phase":
|
||||
phase_attrs = get_current_price_phase_attributes(coordinator, time=time)
|
||||
if phase_attrs:
|
||||
attributes.update(phase_attrs)
|
||||
|
||||
elif key == "next_price_phase":
|
||||
next_phase_attrs = get_next_price_phase_attributes(coordinator, time=time)
|
||||
if next_phase_attrs:
|
||||
attributes.update(next_phase_attrs)
|
||||
|
||||
# For current_interval_price_level, add the original level as attribute
|
||||
if key == "current_interval_price_level" and cached_data.get("last_price_level") is not None:
|
||||
attributes["level_id"] = cached_data["last_price_level"]
|
||||
|
|
|
|||
|
|
@ -107,3 +107,128 @@ def get_day_pattern_attributes(
|
|||
attrs["segments"] = segments
|
||||
|
||||
return attrs or None
|
||||
|
||||
|
||||
def get_current_price_phase_attributes(
|
||||
coordinator: TibberPricesDataUpdateCoordinator,
|
||||
*,
|
||||
time: TibberPricesTimeService,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Build attributes for the current_price_phase sensor.
|
||||
|
||||
Returns details of the monotone segment that covers the current time,
|
||||
plus contextual position info and the full list of all today's segments.
|
||||
|
||||
Args:
|
||||
coordinator: The data update coordinator.
|
||||
time: TibberPricesTimeService instance.
|
||||
|
||||
Returns:
|
||||
Attribute dict or None if data is unavailable.
|
||||
|
||||
"""
|
||||
if not coordinator.data:
|
||||
return None
|
||||
|
||||
day_patterns = coordinator.data.get("dayPatterns")
|
||||
if not day_patterns:
|
||||
return None
|
||||
|
||||
today_data: dict[str, Any] | None = day_patterns.get("today")
|
||||
if not today_data:
|
||||
return None
|
||||
|
||||
segments: list[dict[str, Any]] | None = today_data.get("segments")
|
||||
if not segments:
|
||||
return None
|
||||
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
now = time.now()
|
||||
current_index: int | None = None
|
||||
for i, segment in enumerate(segments):
|
||||
seg_start_str: str | None = segment.get("start")
|
||||
if not seg_start_str:
|
||||
continue
|
||||
seg_start = parse_datetime(seg_start_str)
|
||||
if seg_start is not None and now >= seg_start:
|
||||
current_index = i
|
||||
|
||||
if current_index is None:
|
||||
return None
|
||||
|
||||
seg = segments[current_index]
|
||||
attrs: dict[str, Any] = {
|
||||
"start": seg.get("start"),
|
||||
"end": seg.get("end"),
|
||||
"price_min": seg.get("price_min"),
|
||||
"price_max": seg.get("price_max"),
|
||||
"price_mean": seg.get("price_mean"),
|
||||
"segment_index": current_index,
|
||||
"segment_count": len(segments),
|
||||
"all_segments": segments,
|
||||
}
|
||||
return attrs
|
||||
|
||||
|
||||
def get_next_price_phase_attributes(
|
||||
coordinator: TibberPricesDataUpdateCoordinator,
|
||||
*,
|
||||
time: TibberPricesTimeService,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Build attributes for the next_price_phase sensor.
|
||||
|
||||
Returns details of the segment that follows the currently active one,
|
||||
including its start time (useful for scheduling automations).
|
||||
|
||||
Args:
|
||||
coordinator: The data update coordinator.
|
||||
time: TibberPricesTimeService instance.
|
||||
|
||||
Returns:
|
||||
Attribute dict or None if no next segment exists.
|
||||
|
||||
"""
|
||||
if not coordinator.data:
|
||||
return None
|
||||
|
||||
day_patterns = coordinator.data.get("dayPatterns")
|
||||
if not day_patterns:
|
||||
return None
|
||||
|
||||
today_data: dict[str, Any] | None = day_patterns.get("today")
|
||||
if not today_data:
|
||||
return None
|
||||
|
||||
segments: list[dict[str, Any]] | None = today_data.get("segments")
|
||||
if not segments:
|
||||
return None
|
||||
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
now = time.now()
|
||||
current_index: int | None = None
|
||||
for i, segment in enumerate(segments):
|
||||
seg_start_str: str | None = segment.get("start")
|
||||
if not seg_start_str:
|
||||
continue
|
||||
seg_start = parse_datetime(seg_start_str)
|
||||
if seg_start is not None and now >= seg_start:
|
||||
current_index = i
|
||||
|
||||
if current_index is None or current_index + 1 >= len(segments):
|
||||
return None
|
||||
|
||||
next_seg = segments[current_index + 1]
|
||||
attrs: dict[str, Any] = {
|
||||
"start": next_seg.get("start"),
|
||||
"end": next_seg.get("end"),
|
||||
"price_min": next_seg.get("price_min"),
|
||||
"price_max": next_seg.get("price_max"),
|
||||
"price_mean": next_seg.get("price_mean"),
|
||||
"segment_index": current_index + 1,
|
||||
"segment_count": len(segments),
|
||||
}
|
||||
return attrs
|
||||
|
|
|
|||
|
|
@ -26,19 +26,29 @@ def _hours_to_minutes(state_value: Any) -> int | None:
|
|||
|
||||
def _is_timing_or_volatility_sensor(key: str) -> bool:
|
||||
"""Check if sensor is a timing or volatility sensor."""
|
||||
return key.endswith("_volatility") or (
|
||||
key.startswith(("best_price_", "peak_price_"))
|
||||
and any(
|
||||
suffix in key
|
||||
for suffix in [
|
||||
"end_time",
|
||||
"remaining_minutes",
|
||||
"progress",
|
||||
"next_start_time",
|
||||
"next_in_minutes",
|
||||
]
|
||||
)
|
||||
if key.endswith("_volatility"):
|
||||
return True
|
||||
# best/peak price timing sensors
|
||||
if key.startswith(("best_price_", "peak_price_")) and any(
|
||||
suffix in key for suffix in ["end_time", "remaining_minutes", "progress", "next_start_time", "next_in_minutes"]
|
||||
):
|
||||
return True
|
||||
# price phase timing sensors
|
||||
_PHASE_TIMING_KEYS = frozenset(
|
||||
{
|
||||
"current_price_phase_end_time",
|
||||
"current_price_phase_remaining_minutes",
|
||||
"current_price_phase_duration",
|
||||
"current_price_phase_progress",
|
||||
"next_rising_phase_start_time",
|
||||
"next_falling_phase_start_time",
|
||||
"next_flat_phase_start_time",
|
||||
"next_rising_phase_in_minutes",
|
||||
"next_falling_phase_in_minutes",
|
||||
"next_flat_phase_in_minutes",
|
||||
}
|
||||
)
|
||||
return key in _PHASE_TIMING_KEYS
|
||||
|
||||
|
||||
def add_period_timing_attributes(
|
||||
|
|
@ -63,7 +73,8 @@ def add_period_timing_attributes(
|
|||
|
||||
"""
|
||||
# Determine if this is a quarter-hour or 30-second update sensor
|
||||
is_quarter_hour_sensor = key.endswith(("_end_time", "_next_start_time"))
|
||||
# Includes *_start_time to catch next_[type]_phase_start_time keys
|
||||
is_quarter_hour_sensor = key.endswith(("_end_time", "_next_start_time", "_start_time"))
|
||||
|
||||
now = time.now()
|
||||
|
||||
|
|
@ -86,9 +97,11 @@ def add_period_timing_attributes(
|
|||
if minute_value is not None:
|
||||
if key.endswith("period_duration"):
|
||||
attributes["period_duration_minutes"] = minute_value
|
||||
elif "phase_duration" in key:
|
||||
attributes["phase_duration_minutes"] = minute_value
|
||||
elif key.endswith("remaining_minutes"):
|
||||
attributes["remaining_minutes"] = minute_value
|
||||
elif key.endswith("next_in_minutes"):
|
||||
elif key.endswith("in_minutes"):
|
||||
attributes["next_in_minutes"] = minute_value
|
||||
|
||||
# Add icon_color for dynamic styling
|
||||
|
|
|
|||
|
|
@ -137,3 +137,247 @@ class TibberPricesMetadataCalculator(TibberPricesBaseCalculator):
|
|||
return None
|
||||
|
||||
return day_data.get("pattern")
|
||||
|
||||
def get_current_price_phase_value(self) -> str | None:
|
||||
"""
|
||||
Get the current intra-day price phase (rising / falling / flat).
|
||||
|
||||
Finds the monotone segment in today's day-pattern that covers the
|
||||
current time and returns its type string.
|
||||
|
||||
Returns:
|
||||
"rising", "falling", or "flat", or None if data is unavailable.
|
||||
|
||||
"""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
day_patterns = self.coordinator.data.get("dayPatterns")
|
||||
if not day_patterns:
|
||||
return None
|
||||
|
||||
today_data = day_patterns.get("today")
|
||||
if not today_data:
|
||||
return None
|
||||
|
||||
segments: list[dict] | None = today_data.get("segments")
|
||||
if not segments:
|
||||
return None
|
||||
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
now = self.coordinator.time.now()
|
||||
current_segment: dict | None = None
|
||||
for segment in segments:
|
||||
seg_start_str: str | None = segment.get("start")
|
||||
if not seg_start_str:
|
||||
continue
|
||||
seg_start = parse_datetime(seg_start_str)
|
||||
if seg_start is not None and now >= seg_start:
|
||||
current_segment = segment
|
||||
|
||||
if current_segment is None:
|
||||
return None
|
||||
|
||||
return current_segment.get("type")
|
||||
|
||||
def get_next_price_phase_value(self) -> str | None:
|
||||
"""
|
||||
Get the next intra-day price phase (rising / falling / flat).
|
||||
|
||||
Finds the monotone segment in today's day-pattern that starts after
|
||||
the current segment and returns its type string.
|
||||
|
||||
Returns:
|
||||
"rising", "falling", or "flat", or None if no next segment exists.
|
||||
|
||||
"""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
day_patterns = self.coordinator.data.get("dayPatterns")
|
||||
if not day_patterns:
|
||||
return None
|
||||
|
||||
today_data = day_patterns.get("today")
|
||||
if not today_data:
|
||||
return None
|
||||
|
||||
segments: list[dict] | None = today_data.get("segments")
|
||||
if not segments:
|
||||
return None
|
||||
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
now = self.coordinator.time.now()
|
||||
current_index: int | None = None
|
||||
for i, segment in enumerate(segments):
|
||||
seg_start_str: str | None = segment.get("start")
|
||||
if not seg_start_str:
|
||||
continue
|
||||
seg_start = parse_datetime(seg_start_str)
|
||||
if seg_start is not None and now >= seg_start:
|
||||
current_index = i
|
||||
|
||||
if current_index is None or current_index + 1 >= len(segments):
|
||||
return None
|
||||
|
||||
return segments[current_index + 1].get("type")
|
||||
|
||||
def _find_current_segment(self) -> tuple[int, list[dict]] | tuple[None, None]:
|
||||
"""
|
||||
Find the currently active segment in today's day pattern.
|
||||
|
||||
Returns:
|
||||
Tuple of (current_index, segments) or (None, None) if unavailable.
|
||||
|
||||
"""
|
||||
if not self.coordinator.data:
|
||||
return None, None
|
||||
|
||||
day_patterns = self.coordinator.data.get("dayPatterns")
|
||||
if not day_patterns:
|
||||
return None, None
|
||||
|
||||
today_data = day_patterns.get("today")
|
||||
if not today_data:
|
||||
return None, None
|
||||
|
||||
segments: list[dict] | None = today_data.get("segments")
|
||||
if not segments:
|
||||
return None, None
|
||||
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
now = self.coordinator.time.now()
|
||||
current_index: int | None = None
|
||||
for i, segment in enumerate(segments):
|
||||
seg_start_str: str | None = segment.get("start")
|
||||
if not seg_start_str:
|
||||
continue
|
||||
seg_start = parse_datetime(seg_start_str)
|
||||
if seg_start is not None and now >= seg_start:
|
||||
current_index = i
|
||||
|
||||
if current_index is None:
|
||||
return None, None
|
||||
|
||||
return current_index, segments
|
||||
|
||||
def get_price_phase_timing_value(self, value_type: str) -> object:
|
||||
"""
|
||||
Get timing-related values for the current intra-day price phase segment.
|
||||
|
||||
Args:
|
||||
value_type: One of "end_time", "remaining_minutes", "duration", "progress".
|
||||
|
||||
Returns:
|
||||
- datetime for "end_time"
|
||||
- int/float for duration/remaining/progress (0 when no active segment)
|
||||
- None for timestamps when no segment is available
|
||||
|
||||
"""
|
||||
current_index, segments = self._find_current_segment()
|
||||
if current_index is None or segments is None:
|
||||
return 0 if value_type in ("remaining_minutes", "duration", "progress") else None
|
||||
|
||||
seg = segments[current_index]
|
||||
time = self.coordinator.time
|
||||
|
||||
if value_type == "end_time":
|
||||
end_str: str | None = seg.get("end")
|
||||
if not end_str:
|
||||
return None
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
return parse_datetime(end_str)
|
||||
|
||||
if value_type == "remaining_minutes":
|
||||
end_str = seg.get("end")
|
||||
if not end_str:
|
||||
return 0
|
||||
minutes = time.minutes_until_rounded(end_str)
|
||||
return max(0, minutes)
|
||||
|
||||
if value_type == "duration":
|
||||
start_str: str | None = seg.get("start")
|
||||
end_str = seg.get("end")
|
||||
if not start_str or not end_str:
|
||||
return None
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
start_dt = parse_datetime(start_str)
|
||||
end_dt = parse_datetime(end_str)
|
||||
if start_dt is None or end_dt is None:
|
||||
return None
|
||||
return max(0.0, (end_dt - start_dt).total_seconds() / 60)
|
||||
|
||||
if value_type == "progress":
|
||||
start_str = seg.get("start")
|
||||
end_str = seg.get("end")
|
||||
if not start_str or not end_str:
|
||||
return 0
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
start_dt = parse_datetime(start_str)
|
||||
end_dt = parse_datetime(end_str)
|
||||
if start_dt is None or end_dt is None:
|
||||
return 0
|
||||
now = time.now()
|
||||
total = (end_dt - start_dt).total_seconds()
|
||||
if total <= 0:
|
||||
return 0
|
||||
elapsed = (now - start_dt).total_seconds()
|
||||
return min(100.0, max(0.0, (elapsed / total) * 100))
|
||||
|
||||
return None
|
||||
|
||||
def get_next_phase_of_type_value(self, phase_type: str, value_type: str) -> object:
|
||||
"""
|
||||
Find the next intra-day phase of a given type and return its timing.
|
||||
|
||||
Searches remaining segments for today (after current) plus all of tomorrow.
|
||||
|
||||
Args:
|
||||
phase_type: "rising", "falling", or "flat".
|
||||
value_type: "start_time" (returns datetime) or "in_minutes" (returns int).
|
||||
|
||||
Returns:
|
||||
- datetime for "start_time"
|
||||
- int for "in_minutes"
|
||||
- None if no future segment of this type exists
|
||||
|
||||
"""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
day_patterns = self.coordinator.data.get("dayPatterns")
|
||||
if not day_patterns:
|
||||
return None
|
||||
|
||||
current_index, today_segments = self._find_current_segment()
|
||||
if today_segments is None:
|
||||
return None
|
||||
|
||||
# Remaining segments in today (after current index)
|
||||
start_idx = (current_index + 1) if current_index is not None else 0
|
||||
remaining_today: list[dict] = today_segments[start_idx:]
|
||||
|
||||
# All segments in tomorrow (if available)
|
||||
tomorrow_data = day_patterns.get("tomorrow")
|
||||
tomorrow_segments: list[dict] = tomorrow_data.get("segments", []) if tomorrow_data else []
|
||||
|
||||
# Search in order: remaining today → all tomorrow
|
||||
for segment in (*remaining_today, *tomorrow_segments):
|
||||
if segment.get("type") == phase_type:
|
||||
start_str: str | None = segment.get("start")
|
||||
if not start_str:
|
||||
continue
|
||||
if value_type == "start_time":
|
||||
from homeassistant.util.dt import parse_datetime # noqa: PLC0415
|
||||
|
||||
return parse_datetime(start_str)
|
||||
if value_type == "in_minutes":
|
||||
return self.coordinator.time.minutes_until_rounded(start_str)
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1045,6 +1045,137 @@ DAY_PATTERN_SENSORS = (
|
|||
state_class=None,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="current_price_phase",
|
||||
translation_key="current_price_phase",
|
||||
icon="mdi:chart-timeline-variant",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["rising", "falling", "flat"],
|
||||
state_class=None,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_price_phase",
|
||||
translation_key="next_price_phase",
|
||||
icon="mdi:chart-timeline-variant",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["rising", "falling", "flat"],
|
||||
state_class=None,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
)
|
||||
|
||||
# 8b. PRICE PHASE TIMING SENSORS (current phase duration/progress + next-phase-by-type)
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# When current phase is active:
|
||||
# - end_time: Timestamp when current phase ends
|
||||
# - remaining_minutes: Minutes until current phase ends
|
||||
# - duration: Total length of current phase (disabled by default)
|
||||
# - progress: Percentage of current phase completed (disabled by default)
|
||||
#
|
||||
# Next occurrence of a specific phase type (after current segment, today or tomorrow):
|
||||
# - next_*_phase_start_time: Timestamp when next rising/falling/flat phase starts
|
||||
# - next_*_phase_in_minutes: Minutes until that phase starts
|
||||
#
|
||||
# All return None/Unknown when no segment data is available.
|
||||
|
||||
PRICE_PHASE_TIMING_SENSORS = (
|
||||
SensorEntityDescription(
|
||||
key="current_price_phase_end_time",
|
||||
translation_key="current_price_phase_end_time",
|
||||
icon="mdi:clock-end",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None, # Timestamps: no statistics
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="current_price_phase_remaining_minutes",
|
||||
translation_key="current_price_phase_remaining_minutes",
|
||||
icon="mdi:timer-sand",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
state_class=None, # Countdown timers excluded from statistics
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="current_price_phase_duration",
|
||||
translation_key="current_price_phase_duration",
|
||||
icon="mdi:timer",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
state_class=None, # Duration not needed in long-term statistics
|
||||
suggested_display_precision=2,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="current_price_phase_progress",
|
||||
translation_key="current_price_phase_progress",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None, # Progress counter: no statistics
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# Next occurrence of each phase type (across remaining today + tomorrow)
|
||||
SensorEntityDescription(
|
||||
key="next_rising_phase_start_time",
|
||||
translation_key="next_rising_phase_start_time",
|
||||
icon="mdi:trending-up",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_falling_phase_start_time",
|
||||
translation_key="next_falling_phase_start_time",
|
||||
icon="mdi:trending-down",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_flat_phase_start_time",
|
||||
translation_key="next_flat_phase_start_time",
|
||||
icon="mdi:trending-neutral",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_rising_phase_in_minutes",
|
||||
translation_key="next_rising_phase_in_minutes",
|
||||
icon="mdi:timer-outline",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
state_class=None,
|
||||
suggested_display_precision=2,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_falling_phase_in_minutes",
|
||||
translation_key="next_falling_phase_in_minutes",
|
||||
icon="mdi:timer-outline",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
state_class=None,
|
||||
suggested_display_precision=2,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_flat_phase_in_minutes",
|
||||
translation_key="next_flat_phase_in_minutes",
|
||||
icon="mdi:timer-outline",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
state_class=None,
|
||||
suggested_display_precision=2,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
# 9. DIAGNOSTIC SENSORS (data availability and metadata)
|
||||
|
|
@ -1239,5 +1370,6 @@ ENTITY_DESCRIPTIONS = (
|
|||
*BEST_PRICE_TIMING_SENSORS,
|
||||
*PEAK_PRICE_TIMING_SENSORS,
|
||||
*DAY_PATTERN_SENSORS,
|
||||
*PRICE_PHASE_TIMING_SENSORS,
|
||||
*DIAGNOSTIC_SENSORS,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -242,6 +242,29 @@ def get_value_getter_mapping(
|
|||
"day_pattern_yesterday": lambda: metadata_calculator.get_day_pattern_value("yesterday"),
|
||||
"day_pattern_today": lambda: metadata_calculator.get_day_pattern_value("today"),
|
||||
"day_pattern_tomorrow": lambda: metadata_calculator.get_day_pattern_value("tomorrow"),
|
||||
"current_price_phase": lambda: metadata_calculator.get_current_price_phase_value(),
|
||||
"next_price_phase": lambda: metadata_calculator.get_next_price_phase_value(),
|
||||
# Price phase timing sensors (current phase duration/progress + next-phase-by-type)
|
||||
"current_price_phase_end_time": lambda: metadata_calculator.get_price_phase_timing_value("end_time"),
|
||||
"current_price_phase_remaining_minutes": lambda: metadata_calculator.get_price_phase_timing_value(
|
||||
"remaining_minutes"
|
||||
),
|
||||
"current_price_phase_duration": lambda: metadata_calculator.get_price_phase_timing_value("duration"),
|
||||
"current_price_phase_progress": lambda: metadata_calculator.get_price_phase_timing_value("progress"),
|
||||
"next_rising_phase_start_time": lambda: metadata_calculator.get_next_phase_of_type_value(
|
||||
"rising", "start_time"
|
||||
),
|
||||
"next_falling_phase_start_time": lambda: metadata_calculator.get_next_phase_of_type_value(
|
||||
"falling", "start_time"
|
||||
),
|
||||
"next_flat_phase_start_time": lambda: metadata_calculator.get_next_phase_of_type_value("flat", "start_time"),
|
||||
"next_rising_phase_in_minutes": lambda: metadata_calculator.get_next_phase_of_type_value(
|
||||
"rising", "in_minutes"
|
||||
),
|
||||
"next_falling_phase_in_minutes": lambda: metadata_calculator.get_next_phase_of_type_value(
|
||||
"falling", "in_minutes"
|
||||
),
|
||||
"next_flat_phase_in_minutes": lambda: metadata_calculator.get_next_phase_of_type_value("flat", "in_minutes"),
|
||||
# Volatility sensors (via VolatilityCalculator)
|
||||
"today_volatility": lambda: volatility_calculator.get_volatility_value(volatility_type="today"),
|
||||
"tomorrow_volatility": lambda: volatility_calculator.get_volatility_value(volatility_type="tomorrow"),
|
||||
|
|
|
|||
|
|
@ -1211,7 +1211,7 @@
|
|||
},
|
||||
"currency_display_mode_changed": {
|
||||
"title": "Währungsanzeigeeinheit für {home_name} geändert",
|
||||
"description": "Du hast den Währungsanzeigemodus für **{home_name}** geändert. Alle Preissensor-Werte und -Attribute verwenden jetzt die neue Einheit (z.B. 25,34 ct → 0,2534 € oder umgekehrt).\n\nDer Recorder von Home Assistant zeigt separat einen Dialog **„Die Einheit hat sich geändert“** für betroffene Sensoren — das kann einige Minuten dauern oder bis zum nächsten Statistik-Durchlauf (Warnungen im Log erscheinen früher). Wähle dann **Alle alten Statistikdaten löschen** für einen sauberen Neustart. Wähle nicht „Einheit aktualisieren ohne Konvertierung“: das benennt die alten Zahlen nur um, ohne die Werte anzupassen, und macht die historischen Daten inhaltlich falsch.\n\n**Manuell prüfen:**\n\n1. **Automationen & Templates:** Aktualisiere alle Automationen und Template-Sensoren mit numerischen Preis-Schwellwerten.\n2. **Dashboard-Karten:** Aktualisiere alle Karten mit fest codierten Schwellwerten oder Einheitenbezeichnungen.\n\nSobald du deine Automationen, Dashboards und Statistiken überprüft hast, gehe zu **Einstellungen → Integrationen → Tibber → Konfigurieren → Anzeigeeinstellungen** und speichere (auch ohne Änderungen). Nur so kannst du diese Meldung dauerhaft schließen."
|
||||
"description": "Du hast den Währungsanzeigemodus für **{home_name}** geändert. Alle Preissensor-Werte und -Attribute verwenden jetzt die neue Einheit (z.B. 25,34 ct → 0,2534 € oder umgekehrt).\n\nDer Recorder von Home Assistant zeigt separat einen Dialog **„Die Einheit hat sich geändert“** für betroffene Sensoren — das kann einige Minuten dauern oder bis zum nächsten Statistik-Durchlauf (Warnungen im Log erscheinen früher). Wähle dann **Alle alten Statistikdaten löschen** für einen sauberen Neustart. Wähle nicht „Einheit aktualisieren ohne Konvertierung“: das benennt die alten Zahlen nur um, ohne die Werte anzupassen, und macht die historischen Daten inhaltlich falsch.\n\n**Manuell prüfen:**\n\n1. **Automationen & Templates:** Aktualisiere alle Automationen und Template-Sensoren mit numerischen Preis-Schwellwerten.\n2. **Dashboard-Karten:** Aktualisiere alle Karten mit fest codierten Schwellwerten oder Einheitenbezeichnungen.\n\nSchließe diese Meldung, sobald du deine Automationen, Dashboards und Statistiken überprüft hast."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
|||
|
|
@ -1211,7 +1211,7 @@
|
|||
},
|
||||
"currency_display_mode_changed": {
|
||||
"title": "Currency display unit changed for {home_name}",
|
||||
"description": "You changed the currency display mode for **{home_name}**. All price sensor values and attributes now use the new unit (e.g. 25.34 ct → 0.2534 € or vice versa).\n\nHome Assistant’s Recorder will separately show a **“The unit has changed”** dialog for affected sensors — this may take a few minutes or until the next statistics run (log warnings appear earlier). When it appears, choose **Delete all old statistic data** to start fresh. Do not choose “Update the unit without converting”: that re-labels the old numbers without adjusting their values, making the historic data factually incorrect.\n\n**Review manually:**\n\n1. **Automations & Templates:** Update all automations and template sensors that use numeric price thresholds.\n2. **Dashboard Cards:** Update any cards with hardcoded thresholds or unit labels.\n\nOnce you have reviewed your automations, dashboards, and statistics, go to **Settings → Integrations → Tibber → Configure → Display Settings** and save (without any changes if you like). This is the only way to permanently close this notification."
|
||||
"description": "You changed the currency display mode for **{home_name}**. All price sensor values and attributes now use the new unit (e.g. 25.34 ct → 0.2534 € or vice versa).\n\nHome Assistant’s Recorder will separately show a **“The unit has changed”** dialog for affected sensors — this may take a few minutes or until the next statistics run (log warnings appear earlier). When it appears, choose **Delete all old statistic data** to start fresh. Do not choose “Update the unit without converting”: that re-labels the old numbers without adjusting their values, making the historic data factually incorrect.\n\n**Review manually:**\n\n1. **Automations & Templates:** Update all automations and template sensors that use numeric price thresholds.\n2. **Dashboard Cards:** Update any cards with hardcoded thresholds or unit labels.\n\nDismiss this notice once you have reviewed your automations, dashboards, and statistics."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
|||
|
|
@ -1211,7 +1211,7 @@
|
|||
},
|
||||
"currency_display_mode_changed": {
|
||||
"title": "Valutavisningsenhet endret for {home_name}",
|
||||
"description": "Du endret valutavisningsmodusen for **{home_name}**. Alle prissensorverdier og -attributter bruker nå den nye enheten (f.eks. 25,34 øre → 0,2534 kr eller omvendt).\n\nHome Assistants Recorder viser separat en **„Enheten har endret seg“**-dialog for berørte sensorer — dette kan ta noen minutter eller til neste statistikkjøring (advarsler i loggen dukker opp tidligere). Når den vises, velg **Slett alle gamle statistikkdata** for en ren start. Ikke velg „Oppdater enheten uten konvertering“: det beholder de gamle tallene med ny enhet uten å justere verdiene, og gjør de historiske dataene faktisk feil.\n\n**Gjennomgå manuelt:**\n\n1. **Automatiseringer & maler:** Oppdater alle automatiseringer og malsensorer som bruker numeriske pristerskler.\n2. **Dashboard-kort:** Oppdater kort med hardkodede terskelverdier eller enhetsetiketter.\n\nNår du har gjennomgått automatiseringer, dashboards og statistikk, gå til **Innstillinger → Integrasjoner → Tibber → Konfigurer → Visningsinnstillinger** og lagre (uten endringer om ønskelig). Dette er den eneste måten å lukke denne meldingen permanent."
|
||||
"description": "Du endret valutavisningsmodusen for **{home_name}**. Alle prissensorverdier og -attributter bruker nå den nye enheten (f.eks. 25,34 øre → 0,2534 kr eller omvendt).\n\nHome Assistants Recorder viser separat en **„Enheten har endret seg“**-dialog for berørte sensorer — dette kan ta noen minutter eller til neste statistikkjøring (advarsler i loggen dukker opp tidligere). Når den vises, velg **Slett alle gamle statistikkdata** for en ren start. Ikke velg „Oppdater enheten uten konvertering“: det beholder de gamle tallene med ny enhet uten å justere verdiene, og gjør de historiske dataene faktisk feil.\n\n**Gjennomgå manuelt:**\n\n1. **Automatiseringer & maler:** Oppdater alle automatiseringer og malsensorer som bruker numeriske pristerskler.\n2. **Dashboard-kort:** Oppdater kort med hardkodede terskelverdier eller enhetsetiketter.\n\nAvvis denne meldingen når du har gjennomgått automatiseringer, dashboards og statistikk."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
|||
|
|
@ -1211,7 +1211,7 @@
|
|||
},
|
||||
"currency_display_mode_changed": {
|
||||
"title": "Valutaweergave-eenheid gewijzigd voor {home_name}",
|
||||
"description": "Je hebt de valutaweergavemodus voor **{home_name}** gewijzigd. Alle prijssensorwaarden en -attributen gebruiken nu de nieuwe eenheid (bijv. 25,34 ct → 0,2534 € of andersom).\n\nHome Assistant’s Recorder toont afzonderlijk een **„De eenheid is gewijzigd“**-dialoogvenster voor getroffen sensoren — dit kan enkele minuten duren of tot de volgende statistiekenrun (logwaarschuwingen verschijnen eerder). Kies dan **Alle oude statistiekgegevens verwijderen** voor een schone start. Kies niet „Eenheid bijwerken zonder conversie“: dat hernoemt de oude getallen zonder de waarden aan te passen, waardoor de historische gegevens inhoudelijk onjuist worden.\n\n**Handmatig controleren:**\n\n1. **Automatiseringen & templates:** Werk alle automatiseringen en template-sensoren bij die numerieke prijsdrempels gebruiken.\n2. **Dashboard-kaarten:** Werk kaarten bij met hardgecodeerde drempelwaarden of eenheidslabels.\n\nZodra je je automatiseringen, dashboards en statistieken hebt gecontroleerd, ga naar **Instellingen → Integraties → Tibber → Configureren → Weergave-instellingen** en sla op (zonder wijzigingen indien gewenst). Dit is de enige manier om deze melding permanent te sluiten."
|
||||
"description": "Je hebt de valutaweergavemodus voor **{home_name}** gewijzigd. Alle prijssensorwaarden en -attributen gebruiken nu de nieuwe eenheid (bijv. 25,34 ct → 0,2534 € of andersom).\n\nHome Assistant’s Recorder toont afzonderlijk een **„De eenheid is gewijzigd“**-dialoogvenster voor getroffen sensoren — dit kan enkele minuten duren of tot de volgende statistiekenrun (logwaarschuwingen verschijnen eerder). Kies dan **Alle oude statistiekgegevens verwijderen** voor een schone start. Kies niet „Eenheid bijwerken zonder conversie“: dat hernoemt de oude getallen zonder de waarden aan te passen, waardoor de historische gegevens inhoudelijk onjuist worden.\n\n**Handmatig controleren:**\n\n1. **Automatiseringen & templates:** Werk alle automatiseringen en template-sensoren bij die numerieke prijsdrempels gebruiken.\n2. **Dashboard-kaarten:** Werk kaarten bij met hardgecodeerde drempelwaarden of eenheidslabels.\n\nSluit deze melding nadat je je automatiseringen, dashboards en statistieken hebt gecontroleerd."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
|||
|
|
@ -1211,7 +1211,7 @@
|
|||
},
|
||||
"currency_display_mode_changed": {
|
||||
"title": "Valutavisningsenhet ändrad för {home_name}",
|
||||
"description": "Du ändrade valutavisningsläget för **{home_name}**. Alla prissensorvärden och -attribut använder nu den nya enheten (t.ex. 25,34 öre → 0,2534 kr eller tvärtom).\n\nHome Assistants Recorder visar separat en **„Enheten har ändrats“**-dialog för berörda sensorer — det kan ta några minuter eller till nästa statistikkörning (loggvarningar dyker upp tidigare). När den visas, välj **Ta bort alla gamla statistikdata** för en ren start. Välj inte „Uppdatera enheten utan konvertering“: det behåller de gamla talen med ny enhet utan att justera värdena, vilket gör historiska data faktiskt felaktiga.\n\n**Granska manuellt:**\n\n1. **Automatiseringar & mallar:** Uppdatera alla automatiseringar och mallsensorer som använder numeriska priströsklar.\n2. **Dashboard-kort:** Uppdatera kort med hårdkodade tröskelvärden eller enhetsetiketter.\n\nNär du har granskat dina automatiseringar, instrumentpaneler och statistik, gå till **Inställningar → Integrationer → Tibber → Konfigurera → Visningsinställningar** och spara (utan ändringar om du vill). Det är det enda sättet att permanent stänga det här meddelandet."
|
||||
"description": "Du ändrade valutavisningsläget för **{home_name}**. Alla prissensorvärden och -attribut använder nu den nya enheten (t.ex. 25,34 öre → 0,2534 kr eller tvärtom).\n\nHome Assistants Recorder visar separat en **„Enheten har ändrats“**-dialog för berörda sensorer — det kan ta några minuter eller till nästa statistikkörning (loggvarningar dyker upp tidigare). När den visas, välj **Ta bort alla gamla statistikdata** för en ren start. Välj inte „Uppdatera enheten utan konvertering“: det behåller de gamla talen med ny enhet utan att justera värdena, vilket gör historiska data faktiskt felaktiga.\n\n**Granska manuellt:**\n\n1. **Automatiseringar & mallar:** Uppdatera alla automatiseringar och mallsensorer som använder numeriska priströsklar.\n2. **Dashboard-kort:** Uppdatera kort med hårdkodade tröskelvärden eller enhetsetiketter.\n\nStäng det här meddelandet när du har granskat dina automatiseringar, instrumentpaneler och statistik."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
|||
|
|
@ -450,9 +450,9 @@ def test_timer_group_sizes() -> None:
|
|||
This isn't a strict requirement, but significant changes in group sizes
|
||||
might indicate accidental additions/removals.
|
||||
"""
|
||||
# As of Nov 2025
|
||||
# As of Apr 2026 (added 5 price-phase countdown sensors)
|
||||
expected_time_sensitive_min = 40 # At least 40 sensors
|
||||
expected_minute_update = 7 # Exactly 7 timing sensors
|
||||
expected_minute_update = 12 # 7 original + 5 price-phase countdown sensors
|
||||
|
||||
assert len(TIME_SENSITIVE_ENTITY_KEYS) >= expected_time_sensitive_min, (
|
||||
f"Expected at least {expected_time_sensitive_min} TIME_SENSITIVE sensors, got {len(TIME_SENSITIVE_ENTITY_KEYS)}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue