mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
feat(sensors): add timing sensors for best_price and peak_price periods
Added 10 new timing sensors (5 for best_price, 5 for peak_price) to track
period timing and progress:
Timestamp sensors (quarter-hour updates):
- best_price_end_time / peak_price_end_time
Shows when current/next period ends (always useful reference time)
- best_price_next_start_time / peak_price_next_start_time
Shows when next period starts (even during active periods)
Countdown sensors (minute updates):
- best_price_remaining_minutes / peak_price_remaining_minutes
Minutes left in current period (0 when inactive)
- best_price_next_in_minutes / peak_price_next_in_minutes
Minutes until next period starts
- best_price_progress / peak_price_progress
Progress percentage through current period (0-100%)
Smart fallback behavior:
- Sensors always show useful values (no 'Unknown' during normal operation)
- Timestamp sensors show current OR next period end/start times
- Countdown sensors return 0 when no period is active
- Grace period: Progress stays at 100% for 60 seconds after period ends
Dynamic visual feedback:
- Progress icons differentiate 3 states at 0%:
* No data: mdi:help-circle-outline (gray)
* Waiting for next period: mdi:timer-pause-outline
* Period just started: mdi:circle-outline
- Progress 1-99%: mdi:circle-slice-1 to mdi:circle-slice-8 (pie chart)
- Timer icons based on urgency (alert/timer/timer-sand/timer-outline)
- Dynamic colors: green (best_price), orange/red (peak_price), gray (disabled)
- icon_color attribute for UI styling
Implementation details:
- Dual update mechanism: quarter-hour (timestamps) + minute (countdowns)
- Period state callbacks: Check if period is currently active
- IconContext dataclass: Reduced function parameters from 6 to 3
- Unit constants: UnitOfTime.MINUTES, PERCENTAGE from homeassistant.const
- Complete translations for 5 languages (de, en, nb, nl, sv)
Impact: Users can now build sophisticated automations based on period timing
('start dishwasher if remaining_minutes > 60'), display countdowns in
dashboards, and get clear visual feedback about period states. All sensors
provide meaningful values at all times, making automation logic simpler.
This commit is contained in:
parent
22165d038d
commit
decca432df
16 changed files with 1107 additions and 25 deletions
|
|
@ -153,6 +153,28 @@ TIME_SENSITIVE_ENTITY_KEYS = frozenset(
|
||||||
# Binary sensors that check if current time is in a period
|
# Binary sensors that check if current time is in a period
|
||||||
"peak_price_period",
|
"peak_price_period",
|
||||||
"best_price_period",
|
"best_price_period",
|
||||||
|
# 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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Entities that require minute-by-minute updates (separate from quarter-hour updates)
|
||||||
|
# These are timing sensors that track countdown/progress within best/peak price periods
|
||||||
|
# Timestamp sensors (end_time, next_start_time) only need quarter-hour updates since periods
|
||||||
|
# can only change at interval boundaries
|
||||||
|
MINUTE_UPDATE_ENTITY_KEYS = frozenset(
|
||||||
|
{
|
||||||
|
# Best Price countdown/progress sensors (need minute updates)
|
||||||
|
"best_price_remaining_minutes",
|
||||||
|
"best_price_progress",
|
||||||
|
"best_price_next_in_minutes",
|
||||||
|
# Peak Price countdown/progress sensors (need minute updates)
|
||||||
|
"peak_price_remaining_minutes",
|
||||||
|
"peak_price_progress",
|
||||||
|
"peak_price_next_in_minutes",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -206,11 +228,19 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
# Quarter-hour entity refresh timer (runs at :00, :15, :30, :45)
|
# Quarter-hour entity refresh timer (runs at :00, :15, :30, :45)
|
||||||
self._quarter_hour_timer_cancel: CALLBACK_TYPE | None = None
|
self._quarter_hour_timer_cancel: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
|
# Minute-by-minute entity refresh timer (runs every minute for timing sensors)
|
||||||
|
self._minute_timer_cancel: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
# Selective listener system for time-sensitive entities
|
# Selective listener system for time-sensitive entities
|
||||||
# Regular listeners update on API data changes, time-sensitive listeners update every 15 minutes
|
# Regular listeners update on API data changes, time-sensitive listeners update every 15 minutes
|
||||||
self._time_sensitive_listeners: list[CALLBACK_TYPE] = []
|
self._time_sensitive_listeners: list[CALLBACK_TYPE] = []
|
||||||
|
|
||||||
|
# Minute-update listener system for timing sensors
|
||||||
|
# These listeners update every minute to track progress/remaining time in periods
|
||||||
|
self._minute_update_listeners: list[CALLBACK_TYPE] = []
|
||||||
|
|
||||||
self._schedule_quarter_hour_refresh()
|
self._schedule_quarter_hour_refresh()
|
||||||
|
self._schedule_minute_refresh()
|
||||||
|
|
||||||
def _log(self, level: str, message: str, *args: Any, **kwargs: Any) -> None:
|
def _log(self, level: str, message: str, *args: Any, **kwargs: Any) -> None:
|
||||||
"""Log with coordinator-specific prefix."""
|
"""Log with coordinator-specific prefix."""
|
||||||
|
|
@ -250,6 +280,39 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
len(self._time_sensitive_listeners),
|
len(self._time_sensitive_listeners),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_minute_update_listener(self, update_callback: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
||||||
|
"""
|
||||||
|
Listen for minute-by-minute updates for timing sensors.
|
||||||
|
|
||||||
|
Timing sensors (like best_price_remaining_minutes, peak_price_progress, etc.) should use this
|
||||||
|
method to receive updates every minute for accurate countdown/progress tracking.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Callback that can be used to remove the listener
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._minute_update_listeners.append(update_callback)
|
||||||
|
|
||||||
|
def remove_listener() -> None:
|
||||||
|
"""Remove update listener."""
|
||||||
|
if update_callback in self._minute_update_listeners:
|
||||||
|
self._minute_update_listeners.remove(update_callback)
|
||||||
|
|
||||||
|
return remove_listener
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_minute_listeners(self) -> None:
|
||||||
|
"""Update all minute-update entities without triggering a full coordinator update."""
|
||||||
|
for update_callback in self._minute_update_listeners:
|
||||||
|
update_callback()
|
||||||
|
|
||||||
|
self._log(
|
||||||
|
"debug",
|
||||||
|
"Updated %d minute-update entities",
|
||||||
|
len(self._minute_update_listeners),
|
||||||
|
)
|
||||||
|
|
||||||
def _schedule_quarter_hour_refresh(self) -> None:
|
def _schedule_quarter_hour_refresh(self) -> None:
|
||||||
"""Schedule the next quarter-hour entity refresh using Home Assistant's time tracking."""
|
"""Schedule the next quarter-hour entity refresh using Home Assistant's time tracking."""
|
||||||
# Cancel any existing timer
|
# Cancel any existing timer
|
||||||
|
|
@ -293,6 +356,35 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
# Static entities (statistics, diagnostics) only update when new API data arrives
|
# Static entities (statistics, diagnostics) only update when new API data arrives
|
||||||
self._async_update_time_sensitive_listeners()
|
self._async_update_time_sensitive_listeners()
|
||||||
|
|
||||||
|
def _schedule_minute_refresh(self) -> None:
|
||||||
|
"""Schedule minute-by-minute entity refresh for timing sensors."""
|
||||||
|
# Cancel any existing timer
|
||||||
|
if self._minute_timer_cancel:
|
||||||
|
self._minute_timer_cancel()
|
||||||
|
self._minute_timer_cancel = None
|
||||||
|
|
||||||
|
# Use Home Assistant's async_track_utc_time_change to trigger every minute at second=1
|
||||||
|
# This ensures timing sensors (remaining_minutes, progress) update accurately
|
||||||
|
self._minute_timer_cancel = async_track_utc_time_change(
|
||||||
|
self.hass,
|
||||||
|
self._handle_minute_refresh,
|
||||||
|
second=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._log(
|
||||||
|
"debug",
|
||||||
|
"Scheduled minute-by-minute refresh for timing sensors (every minute at second=1)",
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_minute_refresh(self, _now: datetime | None = None) -> None:
|
||||||
|
"""Handle minute-by-minute entity refresh for timing sensors."""
|
||||||
|
# Only log at debug level to avoid log spam (this runs every minute)
|
||||||
|
self._log("debug", "Minute refresh triggered for timing sensors")
|
||||||
|
|
||||||
|
# Update only minute-update entities (remaining_minutes, progress, etc.)
|
||||||
|
self._async_update_minute_listeners()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _check_and_handle_midnight_turnover(self, now: datetime) -> bool:
|
def _check_and_handle_midnight_turnover(self, now: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -357,6 +449,10 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
self._quarter_hour_timer_cancel()
|
self._quarter_hour_timer_cancel()
|
||||||
self._quarter_hour_timer_cancel = None
|
self._quarter_hour_timer_cancel = None
|
||||||
|
|
||||||
|
if self._minute_timer_cancel:
|
||||||
|
self._minute_timer_cancel()
|
||||||
|
self._minute_timer_cancel = None
|
||||||
|
|
||||||
def _has_existing_main_coordinator(self) -> bool:
|
def _has_existing_main_coordinator(self) -> bool:
|
||||||
"""Check if there's already a main coordinator in hass.data."""
|
"""Check if there's already a main coordinator in hass.data."""
|
||||||
domain_data = self.hass.data.get(DOMAIN, {})
|
domain_data = self.hass.data.get(DOMAIN, {})
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,56 @@
|
||||||
"description": "Prognose kommender Strompreise",
|
"description": "Prognose kommender Strompreise",
|
||||||
"long_description": "Zeigt kommende Strompreise für zukünftige Intervalle in einem Format, das einfach in Dashboards verwendet werden kann",
|
"long_description": "Zeigt kommende Strompreise für zukünftige Intervalle in einem Format, das einfach in Dashboards verwendet werden kann",
|
||||||
"usage_tips": "Verwenden Sie die Attribute dieser Entität, um kommende Preise in Diagrammen oder benutzerdefinierten Karten anzuzeigen. Greifen Sie entweder auf 'intervals' für alle zukünftigen Intervalle oder auf 'hours' für stündliche Zusammenfassungen zu."
|
"usage_tips": "Verwenden Sie die Attribute dieser Entität, um kommende Preise in Diagrammen oder benutzerdefinierten Karten anzuzeigen. Greifen Sie entweder auf 'intervals' für alle zukünftigen Intervalle oder auf 'hours' für stündliche Zusammenfassungen zu."
|
||||||
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"description": "Wann die aktuelle oder nächste günstige Periode endet",
|
||||||
|
"long_description": "Zeigt den Endzeitstempel der aktuellen günstigen Periode wenn aktiv, oder das Ende der nächsten Periode wenn keine Periode aktiv ist. Zeigt immer eine nützliche Zeitreferenz zur Planung. Gibt nur 'Unbekannt' zurück, wenn keine Perioden konfiguriert sind.",
|
||||||
|
"usage_tips": "Nutze dies, um einen Countdown wie 'Günstige Periode endet in 2 Stunden' (wenn aktiv) oder 'Nächste günstige Periode endet um 14:00' (wenn inaktiv) anzuzeigen. Home Assistant zeigt automatisch relative Zeit für Zeitstempel-Sensoren an."
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"description": "Verbleibende Minuten in aktueller günstiger Periode (0 wenn inaktiv)",
|
||||||
|
"long_description": "Zeigt, wie viele Minuten in der aktuellen günstigen Periode noch verbleiben. Gibt 0 zurück, wenn keine Periode aktiv ist. Aktualisiert sich jede Minute. Prüfe binary_sensor.best_price_period um zu sehen, ob eine Periode aktuell aktiv ist.",
|
||||||
|
"usage_tips": "Perfekt für Automatisierungen: 'Wenn remaining_minutes > 0 UND remaining_minutes < 30, starte Waschmaschine jetzt'. Der Wert 0 macht es einfach zu prüfen, ob eine Periode aktiv ist (Wert > 0) oder nicht (Wert = 0)."
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"description": "Fortschritt durch aktuelle günstige Periode (0% wenn inaktiv)",
|
||||||
|
"long_description": "Zeigt den Fortschritt durch die aktuelle günstige Periode als 0-100%. Gibt 0% zurück, wenn keine Periode aktiv ist. Aktualisiert sich jede Minute. 0% bedeutet Periode gerade gestartet, 100% bedeutet sie endet gleich.",
|
||||||
|
"usage_tips": "Super für visuelle Fortschrittsbalken. Nutze in Automatisierungen: 'Wenn progress > 0 UND progress > 75, sende Benachrichtigung, dass günstige Periode bald endet'. Wert 0 zeigt keine aktive Periode an."
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"description": "Wann die nächste günstige Periode startet",
|
||||||
|
"long_description": "Zeigt, wann die nächste kommende günstige Periode startet. Während einer aktiven Periode zeigt dies den Start der NÄCHSTEN Periode nach der aktuellen. Gibt nur 'Unbekannt' zurück, wenn keine zukünftigen Perioden konfiguriert sind.",
|
||||||
|
"usage_tips": "Immer nützlich für Vorausplanung: 'Nächste günstige Periode startet in 3 Stunden' (egal ob du gerade in einer Periode bist oder nicht). Kombiniere mit Automatisierungen: 'Wenn nächste Startzeit in 10 Minuten ist, sende Benachrichtigung zur Vorbereitung der Waschmaschine'."
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"description": "Minuten bis nächste günstige Periode startet (0 beim Übergang)",
|
||||||
|
"long_description": "Zeigt Minuten bis die nächste günstige Periode startet. Während einer aktiven Periode zeigt dies die Zeit bis zur Periode NACH der aktuellen. Gibt 0 während kurzer Übergangsphasen zurück. Aktualisiert sich jede Minute.",
|
||||||
|
"usage_tips": "Perfekt für 'warte bis günstige Periode' Automatisierungen: 'Wenn next_in_minutes > 0 UND next_in_minutes < 15, warte bevor Geschirrspüler gestartet wird'. Wert > 0 zeigt immer an, dass eine zukünftige Periode geplant ist."
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"description": "Wann die aktuelle oder nächste teure Periode endet",
|
||||||
|
"long_description": "Zeigt den Endzeitstempel der aktuellen teuren Periode wenn aktiv, oder das Ende der nächsten Periode wenn keine Periode aktiv ist. Zeigt immer eine nützliche Zeitreferenz zur Planung. Gibt nur 'Unbekannt' zurück, wenn keine Perioden konfiguriert sind.",
|
||||||
|
"usage_tips": "Nutze dies, um 'Teure Periode endet in 1 Stunde' (wenn aktiv) oder 'Nächste teure Periode endet um 18:00' (wenn inaktiv) anzuzeigen. Kombiniere mit Automatisierungen, um Betrieb nach Spitze fortzusetzen."
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"description": "Verbleibende Minuten in aktueller teurer Periode (0 wenn inaktiv)",
|
||||||
|
"long_description": "Zeigt, wie viele Minuten in der aktuellen teuren Periode noch verbleiben. Gibt 0 zurück, wenn keine Periode aktiv ist. Aktualisiert sich jede Minute. Prüfe binary_sensor.peak_price_period um zu sehen, ob eine Periode aktuell aktiv ist.",
|
||||||
|
"usage_tips": "Nutze in Automatisierungen: 'Wenn remaining_minutes > 60, breche aufgeschobene Ladesitzung ab'. Wert 0 macht es einfach zu unterscheiden zwischen aktiven (Wert > 0) und inaktiven (Wert = 0) Perioden."
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"description": "Fortschritt durch aktuelle teure Periode (0% wenn inaktiv)",
|
||||||
|
"long_description": "Zeigt den Fortschritt durch die aktuelle teure Periode als 0-100%. Gibt 0% zurück, wenn keine Periode aktiv ist. Aktualisiert sich jede Minute.",
|
||||||
|
"usage_tips": "Visueller Fortschrittsindikator in Dashboards. Automatisierung: 'Wenn progress > 0 UND progress > 90, bereite normale Heizplanung vor'. Wert 0 zeigt keine aktive Periode an."
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"description": "Wann die nächste teure Periode startet",
|
||||||
|
"long_description": "Zeigt, wann die nächste kommende teure Periode startet. Während einer aktiven Periode zeigt dies den Start der NÄCHSTEN Periode nach der aktuellen. Gibt nur 'Unbekannt' zurück, wenn keine zukünftigen Perioden konfiguriert sind.",
|
||||||
|
"usage_tips": "Immer nützlich für Planung: 'Nächste teure Periode startet in 2 Stunden'. Automatisierung: 'Wenn nächste Startzeit in 30 Minuten ist, reduziere Heiztemperatur vorsorglich'."
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"description": "Minuten bis nächste teure Periode startet (0 beim Übergang)",
|
||||||
|
"long_description": "Zeigt Minuten bis die nächste teure Periode startet. Während einer aktiven Periode zeigt dies die Zeit bis zur Periode NACH der aktuellen. Gibt 0 während kurzer Übergangsphasen zurück. Aktualisiert sich jede Minute.",
|
||||||
|
"usage_tips": "Präventive Automatisierung: 'Wenn next_in_minutes > 0 UND next_in_minutes < 10, beende aktuellen Ladezyklus jetzt bevor Preise steigen'."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,56 @@
|
||||||
"description": "Forecast of upcoming electricity prices",
|
"description": "Forecast of upcoming electricity prices",
|
||||||
"long_description": "Shows upcoming electricity prices for future intervals in a format that's easy to use in dashboards",
|
"long_description": "Shows upcoming electricity prices for future intervals in a format that's easy to use in dashboards",
|
||||||
"usage_tips": "Use this entity's attributes to display upcoming prices in charts or custom cards. Access either 'intervals' for all future intervals or 'hours' for hourly summaries."
|
"usage_tips": "Use this entity's attributes to display upcoming prices in charts or custom cards. Access either 'intervals' for all future intervals or 'hours' for hourly summaries."
|
||||||
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"description": "When the current or next best price period ends",
|
||||||
|
"long_description": "Shows the end timestamp of the current best price period when active, or the end of the next period when no period is active. Always shows a useful time reference for planning. Returns 'Unknown' only when no periods are configured.",
|
||||||
|
"usage_tips": "Use this to display a countdown like 'Cheap period ends in 2 hours' (when active) or 'Next cheap period ends at 14:00' (when inactive). Home Assistant automatically shows relative time for timestamp sensors."
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"description": "Minutes remaining in current best price period (0 when inactive)",
|
||||||
|
"long_description": "Shows how many minutes are left in the current best price period. Returns 0 when no period is active. Updates every minute. Check binary_sensor.best_price_period to see if a period is currently active.",
|
||||||
|
"usage_tips": "Perfect for automations: 'If remaining_minutes > 0 AND remaining_minutes < 30, start washing machine now'. The value 0 makes it easy to check if a period is active (value > 0) or not (value = 0)."
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"description": "Progress through current best price period (0% when inactive)",
|
||||||
|
"long_description": "Shows progress through the current best price period as 0-100%. Returns 0% when no period is active. Updates every minute. 0% means period just started, 100% means it's about to end.",
|
||||||
|
"usage_tips": "Great for visual progress bars. Use in automations: 'If progress > 0 AND progress > 75, send notification that cheap period is ending soon'. Value 0 indicates no active period."
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"description": "When the next best price period starts",
|
||||||
|
"long_description": "Shows when the next upcoming best price period starts. During an active period, this shows the start of the NEXT period after the current one. Returns 'Unknown' only when no future periods are configured.",
|
||||||
|
"usage_tips": "Always useful for planning ahead: 'Next cheap period starts in 3 hours' (whether you're in a period now or not). Combine with automations: 'When next start time is in 10 minutes, send notification to prepare washing machine'."
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"description": "Minutes until next best price period starts (0 when in transition)",
|
||||||
|
"long_description": "Shows minutes until the next best price period starts. During an active period, shows time until the period AFTER the current one. Returns 0 during brief transition moments. Updates every minute.",
|
||||||
|
"usage_tips": "Perfect for 'wait until cheap period' automations: 'If next_in_minutes > 0 AND next_in_minutes < 15, wait before starting dishwasher'. Value > 0 always indicates a future period is scheduled."
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"description": "When the current or next peak price period ends",
|
||||||
|
"long_description": "Shows the end timestamp of the current peak price period when active, or the end of the next period when no period is active. Always shows a useful time reference for planning. Returns 'Unknown' only when no periods are configured.",
|
||||||
|
"usage_tips": "Use this to display 'Expensive period ends in 1 hour' (when active) or 'Next expensive period ends at 18:00' (when inactive). Combine with automations to resume operations after peak."
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"description": "Minutes remaining in current peak price period (0 when inactive)",
|
||||||
|
"long_description": "Shows how many minutes are left in the current peak price period. Returns 0 when no period is active. Updates every minute. Check binary_sensor.peak_price_period to see if a period is currently active.",
|
||||||
|
"usage_tips": "Use in automations: 'If remaining_minutes > 60, cancel deferred charging session'. Value 0 makes it easy to distinguish active (value > 0) from inactive (value = 0) periods."
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"description": "Progress through current peak price period (0% when inactive)",
|
||||||
|
"long_description": "Shows progress through the current peak price period as 0-100%. Returns 0% when no period is active. Updates every minute.",
|
||||||
|
"usage_tips": "Visual progress indicator in dashboards. Automation: 'If progress > 0 AND progress > 90, prepare to resume normal heating schedule'. Value 0 indicates no active period."
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"description": "When the next peak price period starts",
|
||||||
|
"long_description": "Shows when the next upcoming peak price period starts. During an active period, this shows the start of the NEXT period after the current one. Returns 'Unknown' only when no future periods are configured.",
|
||||||
|
"usage_tips": "Always useful for planning: 'Next expensive period starts in 2 hours'. Automation: 'When next start time is in 30 minutes, reduce heating temperature preemptively'."
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"description": "Minutes until next peak price period starts (0 when in transition)",
|
||||||
|
"long_description": "Shows minutes until the next peak price period starts. During an active period, shows time until the period AFTER the current one. Returns 0 during brief transition moments. Updates every minute.",
|
||||||
|
"usage_tips": "Pre-emptive automation: 'If next_in_minutes > 0 AND next_in_minutes < 10, complete current charging cycle now before prices increase'."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,56 @@
|
||||||
"description": "Prognose for kommende elektrisitetspriser",
|
"description": "Prognose for kommende elektrisitetspriser",
|
||||||
"long_description": "Viser kommende elektrisitetspriser for fremtidige intervaller i et format som er enkelt å bruke i dashboards",
|
"long_description": "Viser kommende elektrisitetspriser for fremtidige intervaller i et format som er enkelt å bruke i dashboards",
|
||||||
"usage_tips": "Bruk denne entitetens attributter til å vise kommende priser i diagrammer eller tilpassede kort. Få tilgang til enten 'intervals' for alle fremtidige intervaller eller 'hours' for timesammendrag."
|
"usage_tips": "Bruk denne entitetens attributter til å vise kommende priser i diagrammer eller tilpassede kort. Få tilgang til enten 'intervals' for alle fremtidige intervaller eller 'hours' for timesammendrag."
|
||||||
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"description": "Når gjeldende eller neste billigperiode slutter",
|
||||||
|
"long_description": "Viser sluttidspunktet for gjeldende billigperiode når aktiv, eller slutten av neste periode når ingen periode er aktiv. Viser alltid en nyttig tidsreferanse for planlegging. Returnerer 'Ukjent' bare når ingen perioder er konfigurert.",
|
||||||
|
"usage_tips": "Bruk dette til å vise en nedtelling som 'Billigperiode slutter om 2 timer' (når aktiv) eller 'Neste billigperiode slutter kl 14:00' (når inaktiv). Home Assistant viser automatisk relativ tid for tidsstempelsensorer."
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"description": "Gjenværende minutter i gjeldende billigperiode (0 når inaktiv)",
|
||||||
|
"long_description": "Viser hvor mange minutter som er igjen i gjeldende billigperiode. Returnerer 0 når ingen periode er aktiv. Oppdateres hvert minutt. Sjekk binary_sensor.best_price_period for å se om en periode er aktiv.",
|
||||||
|
"usage_tips": "Perfekt for automatiseringer: 'Hvis remaining_minutes > 0 OG remaining_minutes < 30, start vaskemaskin nå'. Verdien 0 gjør det enkelt å sjekke om en periode er aktiv (verdi > 0) eller ikke (verdi = 0)."
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"description": "Fremdrift gjennom gjeldende billigperiode (0% når inaktiv)",
|
||||||
|
"long_description": "Viser fremdrift gjennom gjeldende billigperiode som 0-100%. Returnerer 0% når ingen periode er aktiv. Oppdateres hvert minutt. 0% betyr periode nettopp startet, 100% betyr den snart slutter.",
|
||||||
|
"usage_tips": "Flott for visuelle fremdriftslinjer. Bruk i automatiseringer: 'Hvis progress > 0 OG progress > 75, send varsel om at billigperiode snart slutter'. Verdi 0 indikerer ingen aktiv periode."
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"description": "Når neste billigperiode starter",
|
||||||
|
"long_description": "Viser når neste kommende billigperiode starter. Under en aktiv periode viser dette starten av NESTE periode etter den gjeldende. Returnerer 'Ukjent' bare når ingen fremtidige perioder er konfigurert.",
|
||||||
|
"usage_tips": "Alltid nyttig for planlegging: 'Neste billigperiode starter om 3 timer' (enten du er i en periode nå eller ikke). Kombiner med automatiseringer: 'Når neste starttid er om 10 minutter, send varsel for å forberede vaskemaskin'."
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"description": "Minutter til neste billigperiode starter (0 ved overgang)",
|
||||||
|
"long_description": "Viser minutter til neste billigperiode starter. Under en aktiv periode viser dette tiden til perioden ETTER den gjeldende. Returnerer 0 under korte overgangsmomenter. Oppdateres hvert minutt.",
|
||||||
|
"usage_tips": "Perfekt for 'vent til billigperiode' automatiseringer: 'Hvis next_in_minutes > 0 OG next_in_minutes < 15, vent før oppvaskmaskin startes'. Verdi > 0 indikerer alltid at en fremtidig periode er planlagt."
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"description": "Når gjeldende eller neste dyrperiode slutter",
|
||||||
|
"long_description": "Viser sluttidspunktet for gjeldende dyrperiode når aktiv, eller slutten av neste periode når ingen periode er aktiv. Viser alltid en nyttig tidsreferanse for planlegging. Returnerer 'Ukjent' bare når ingen perioder er konfigurert.",
|
||||||
|
"usage_tips": "Bruk dette til å vise 'Dyrperiode slutter om 1 time' (når aktiv) eller 'Neste dyrperiode slutter kl 18:00' (når inaktiv). Kombiner med automatiseringer for å gjenoppta drift etter topp."
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"description": "Gjenværende minutter i gjeldende dyrperiode (0 når inaktiv)",
|
||||||
|
"long_description": "Viser hvor mange minutter som er igjen i gjeldende dyrperiode. Returnerer 0 når ingen periode er aktiv. Oppdateres hvert minutt. Sjekk binary_sensor.peak_price_period for å se om en periode er aktiv.",
|
||||||
|
"usage_tips": "Bruk i automatiseringer: 'Hvis remaining_minutes > 60, avbryt utsatt ladeøkt'. Verdi 0 gjør det enkelt å skille mellom aktive (verdi > 0) og inaktive (verdi = 0) perioder."
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"description": "Fremdrift gjennom gjeldende dyrperiode (0% når inaktiv)",
|
||||||
|
"long_description": "Viser fremdrift gjennom gjeldende dyrperiode som 0-100%. Returnerer 0% når ingen periode er aktiv. Oppdateres hvert minutt.",
|
||||||
|
"usage_tips": "Visuell fremdriftsindikator i dashboards. Automatisering: 'Hvis progress > 0 OG progress > 90, forbered normal varmestyringsplan'. Verdi 0 indikerer ingen aktiv periode."
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"description": "Når neste dyrperiode starter",
|
||||||
|
"long_description": "Viser når neste kommende dyrperiode starter. Under en aktiv periode viser dette starten av NESTE periode etter den gjeldende. Returnerer 'Ukjent' bare når ingen fremtidige perioder er konfigurert.",
|
||||||
|
"usage_tips": "Alltid nyttig for planlegging: 'Neste dyrperiode starter om 2 timer'. Automatisering: 'Når neste starttid er om 30 minutter, reduser varmetemperatur forebyggende'."
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"description": "Minutter til neste dyrperiode starter (0 ved overgang)",
|
||||||
|
"long_description": "Viser minutter til neste dyrperiode starter. Under en aktiv periode viser dette tiden til perioden ETTER den gjeldende. Returnerer 0 under korte overgangsmomenter. Oppdateres hvert minutt.",
|
||||||
|
"usage_tips": "Forebyggende automatisering: 'Hvis next_in_minutes > 0 OG next_in_minutes < 10, fullfør gjeldende ladesyklus nå før prisene øker'."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,56 @@
|
||||||
"description": "Prognose van aanstaande elektriciteitsprijzen",
|
"description": "Prognose van aanstaande elektriciteitsprijzen",
|
||||||
"long_description": "Toont aanstaande elektriciteitsprijzen voor toekomstige intervallen in een formaat dat gemakkelijk te gebruiken is in dashboards",
|
"long_description": "Toont aanstaande elektriciteitsprijzen voor toekomstige intervallen in een formaat dat gemakkelijk te gebruiken is in dashboards",
|
||||||
"usage_tips": "Gebruik de attributen van deze entiteit om aanstaande prijzen weer te geven in grafieken of aangepaste kaarten. Toegang tot 'intervals' voor alle toekomstige intervallen of 'hours' voor uuroverzichten."
|
"usage_tips": "Gebruik de attributen van deze entiteit om aanstaande prijzen weer te geven in grafieken of aangepaste kaarten. Toegang tot 'intervals' voor alle toekomstige intervallen of 'hours' voor uuroverzichten."
|
||||||
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"description": "Wanneer de huidige of volgende goedkope periode eindigt",
|
||||||
|
"long_description": "Toont het eindtijdstempel van de huidige goedkope periode wanneer actief, of het einde van de volgende periode wanneer geen periode actief is. Toont altijd een nuttige tijdreferentie voor planning. Geeft alleen 'Onbekend' terug wanneer geen periodes zijn geconfigureerd.",
|
||||||
|
"usage_tips": "Gebruik dit om een aftelling weer te geven zoals 'Goedkope periode eindigt over 2 uur' (wanneer actief) of 'Volgende goedkope periode eindigt om 14:00' (wanneer inactief). Home Assistant toont automatisch relatieve tijd voor tijdstempelsensoren."
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"description": "Resterende minuten in huidige goedkope periode (0 wanneer inactief)",
|
||||||
|
"long_description": "Toont hoeveel minuten er nog over zijn in de huidige goedkope periode. Geeft 0 terug wanneer geen periode actief is. Werkt elke minuut bij. Controleer binary_sensor.best_price_period om te zien of een periode momenteel actief is.",
|
||||||
|
"usage_tips": "Perfect voor automatiseringen: 'Als remaining_minutes > 0 EN remaining_minutes < 30, start wasmachine nu'. De waarde 0 maakt het gemakkelijk om te controleren of een periode actief is (waarde > 0) of niet (waarde = 0)."
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"description": "Voortgang door huidige goedkope periode (0% wanneer inactief)",
|
||||||
|
"long_description": "Toont de voortgang door de huidige goedkope periode als 0-100%. Geeft 0% terug wanneer geen periode actief is. Werkt elke minuut bij. 0% betekent periode net gestart, 100% betekent het eindigt bijna.",
|
||||||
|
"usage_tips": "Geweldig voor visuele voortgangsbalken. Gebruik in automatiseringen: 'Als progress > 0 EN progress > 75, stuur melding dat goedkope periode bijna eindigt'. Waarde 0 geeft aan dat er geen actieve periode is."
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"description": "Wanneer de volgende goedkope periode begint",
|
||||||
|
"long_description": "Toont wanneer de volgende komende goedkope periode begint. Tijdens een actieve periode toont dit de start van de VOLGENDE periode na de huidige. Geeft alleen 'Onbekend' terug wanneer geen toekomstige periodes zijn geconfigureerd.",
|
||||||
|
"usage_tips": "Altijd nuttig voor vooruitplanning: 'Volgende goedkope periode begint over 3 uur' (of je nu in een periode zit of niet). Combineer met automatiseringen: 'Wanneer volgende starttijd over 10 minuten is, stuur melding om wasmachine voor te bereiden'."
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"description": "Minuten tot volgende goedkope periode begint (0 bij overgang)",
|
||||||
|
"long_description": "Toont minuten tot de volgende goedkope periode begint. Tijdens een actieve periode toont dit de tijd tot de periode NA de huidige. Geeft 0 terug tijdens korte overgangsmomenten. Werkt elke minuut bij.",
|
||||||
|
"usage_tips": "Perfect voor 'wacht tot goedkope periode' automatiseringen: 'Als next_in_minutes > 0 EN next_in_minutes < 15, wacht voordat vaatwasser wordt gestart'. Waarde > 0 geeft altijd aan dat een toekomstige periode is gepland."
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"description": "Wanneer de huidige of volgende dure periode eindigt",
|
||||||
|
"long_description": "Toont het eindtijdstempel van de huidige dure periode wanneer actief, of het einde van de volgende periode wanneer geen periode actief is. Toont altijd een nuttige tijdreferentie voor planning. Geeft alleen 'Onbekend' terug wanneer geen periodes zijn geconfigureerd.",
|
||||||
|
"usage_tips": "Gebruik dit om 'Dure periode eindigt over 1 uur' weer te geven (wanneer actief) of 'Volgende dure periode eindigt om 18:00' (wanneer inactief). Combineer met automatiseringen om activiteiten te hervatten na piek."
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"description": "Resterende minuten in huidige dure periode (0 wanneer inactief)",
|
||||||
|
"long_description": "Toont hoeveel minuten er nog over zijn in de huidige dure periode. Geeft 0 terug wanneer geen periode actief is. Werkt elke minuut bij. Controleer binary_sensor.peak_price_period om te zien of een periode momenteel actief is.",
|
||||||
|
"usage_tips": "Gebruik in automatiseringen: 'Als remaining_minutes > 60, annuleer uitgestelde laadronde'. Waarde 0 maakt het gemakkelijk om onderscheid te maken tussen actieve (waarde > 0) en inactieve (waarde = 0) periodes."
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"description": "Voortgang door huidige dure periode (0% wanneer inactief)",
|
||||||
|
"long_description": "Toont de voortgang door de huidige dure periode als 0-100%. Geeft 0% terug wanneer geen periode actief is. Werkt elke minuut bij.",
|
||||||
|
"usage_tips": "Visuele voortgangsindicator in dashboards. Automatisering: 'Als progress > 0 EN progress > 90, bereid normale verwarmingsplanning voor'. Waarde 0 geeft aan dat er geen actieve periode is."
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"description": "Wanneer de volgende dure periode begint",
|
||||||
|
"long_description": "Toont wanneer de volgende komende dure periode begint. Tijdens een actieve periode toont dit de start van de VOLGENDE periode na de huidige. Geeft alleen 'Onbekend' terug wanneer geen toekomstige periodes zijn geconfigureerd.",
|
||||||
|
"usage_tips": "Altijd nuttig voor planning: 'Volgende dure periode begint over 2 uur'. Automatisering: 'Wanneer volgende starttijd over 30 minuten is, verlaag verwarmingstemperatuur preventief'."
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"description": "Minuten tot volgende dure periode begint (0 bij overgang)",
|
||||||
|
"long_description": "Toont minuten tot de volgende dure periode begint. Tijdens een actieve periode toont dit de tijd tot de periode NA de huidige. Geeft 0 terug tijdens korte overgangsmomenten. Werkt elke minuut bij.",
|
||||||
|
"usage_tips": "Preventieve automatisering: 'Als next_in_minutes > 0 EN next_in_minutes < 10, voltooi huidige laadcyclus nu voordat prijzen stijgen'."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,56 @@
|
||||||
"description": "Prognos för kommande elpriser",
|
"description": "Prognos för kommande elpriser",
|
||||||
"long_description": "Visar kommande elpriser för framtida intervaller i ett format som är enkelt att använda i instrumentpaneler",
|
"long_description": "Visar kommande elpriser för framtida intervaller i ett format som är enkelt att använda i instrumentpaneler",
|
||||||
"usage_tips": "Använd denna enhets attribut för att visa kommande priser i diagram eller anpassade kort. Få åtkomst till antingen 'intervals' för alla framtida intervaller eller 'hours' för timvisa sammanfattningar."
|
"usage_tips": "Använd denna enhets attribut för att visa kommande priser i diagram eller anpassade kort. Få åtkomst till antingen 'intervals' för alla framtida intervaller eller 'hours' för timvisa sammanfattningar."
|
||||||
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"description": "När nuvarande eller nästa billigperiod slutar",
|
||||||
|
"long_description": "Visar sluttidsstämpeln för nuvarande billigperiod när aktiv, eller slutet av nästa period när ingen period är aktiv. Visar alltid en användbar tidsreferens för planering. Returnerar 'Okänt' endast när inga perioder är konfigurerade.",
|
||||||
|
"usage_tips": "Använd detta för att visa en nedräkning som 'Billigperiod slutar om 2 timmar' (när aktiv) eller 'Nästa billigperiod slutar kl 14:00' (när inaktiv). Home Assistant visar automatiskt relativ tid för tidsstämpelsensorer."
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"description": "Återstående minuter i nuvarande billigperiod (0 när inaktiv)",
|
||||||
|
"long_description": "Visar hur många minuter som återstår i nuvarande billigperiod. Returnerar 0 när ingen period är aktiv. Uppdateras varje minut. Kontrollera binary_sensor.best_price_period för att se om en period är aktiv.",
|
||||||
|
"usage_tips": "Perfekt för automationer: 'Om remaining_minutes > 0 OCH remaining_minutes < 30, starta tvättmaskin nu'. Värdet 0 gör det enkelt att kontrollera om en period är aktiv (värde > 0) eller inte (värde = 0)."
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"description": "Framsteg genom nuvarande billigperiod (0% när inaktiv)",
|
||||||
|
"long_description": "Visar framsteg genom nuvarande billigperiod som 0-100%. Returnerar 0% när ingen period är aktiv. Uppdateras varje minut. 0% betyder period just startad, 100% betyder den snart slutar.",
|
||||||
|
"usage_tips": "Bra för visuella framstegsstaplar. Använd i automationer: 'Om progress > 0 OCH progress > 75, skicka meddelande att billigperiod snart slutar'. Värde 0 indikerar ingen aktiv period."
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"description": "När nästa billigperiod startar",
|
||||||
|
"long_description": "Visar när nästa kommande billigperiod startar. Under en aktiv period visar detta starten av NÄSTA period efter den nuvarande. Returnerar 'Okänt' endast när inga framtida perioder är konfigurerade.",
|
||||||
|
"usage_tips": "Alltid användbart för framåtplanering: 'Nästa billigperiod startar om 3 timmar' (oavsett om du är i en period nu eller inte). Kombinera med automationer: 'När nästa starttid är om 10 minuter, skicka meddelande för att förbereda tvättmaskin'."
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"description": "Minuter tills nästa billigperiod startar (0 vid övergång)",
|
||||||
|
"long_description": "Visar minuter tills nästa billigperiod startar. Under en aktiv period visar detta tiden till perioden EFTER den nuvarande. Returnerar 0 under korta övergångsmoment. Uppdateras varje minut.",
|
||||||
|
"usage_tips": "Perfekt för 'vänta tills billigperiod' automationer: 'Om next_in_minutes > 0 OCH next_in_minutes < 15, vänta innan diskmaskin startas'. Värde > 0 indikerar alltid att en framtida period är planerad."
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"description": "När nuvarande eller nästa dyrperiod slutar",
|
||||||
|
"long_description": "Visar sluttidsstämpeln för nuvarande dyrperiod när aktiv, eller slutet av nästa period när ingen period är aktiv. Visar alltid en användbar tidsreferens för planering. Returnerar 'Okänt' endast när inga perioder är konfigurerade.",
|
||||||
|
"usage_tips": "Använd detta för att visa 'Dyrperiod slutar om 1 timme' (när aktiv) eller 'Nästa dyrperiod slutar kl 18:00' (när inaktiv). Kombinera med automationer för att återuppta drift efter topp."
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"description": "Återstående minuter i nuvarande dyrperiod (0 när inaktiv)",
|
||||||
|
"long_description": "Visar hur många minuter som återstår i nuvarande dyrperiod. Returnerar 0 när ingen period är aktiv. Uppdateras varje minut. Kontrollera binary_sensor.peak_price_period för att se om en period är aktiv.",
|
||||||
|
"usage_tips": "Använd i automationer: 'Om remaining_minutes > 60, avbryt uppskjuten laddningssession'. Värde 0 gör det enkelt att skilja mellan aktiva (värde > 0) och inaktiva (värde = 0) perioder."
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"description": "Framsteg genom nuvarande dyrperiod (0% när inaktiv)",
|
||||||
|
"long_description": "Visar framsteg genom nuvarande dyrperiod som 0-100%. Returnerar 0% när ingen period är aktiv. Uppdateras varje minut.",
|
||||||
|
"usage_tips": "Visuell framstegsindikator i instrumentpaneler. Automation: 'Om progress > 0 OCH progress > 90, förbered normal värmeplanering'. Värde 0 indikerar ingen aktiv period."
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"description": "När nästa dyrperiod startar",
|
||||||
|
"long_description": "Visar när nästa kommande dyrperiod startar. Under en aktiv period visar detta starten av NÄSTA period efter den nuvarande. Returnerar 'Okänt' endast när inga framtida perioder är konfigurerade.",
|
||||||
|
"usage_tips": "Alltid användbart för planering: 'Nästa dyrperiod startar om 2 timmar'. Automation: 'När nästa starttid är om 30 minuter, minska värmetemperatur förebyggande'."
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"description": "Minuter tills nästa dyrperiod startar (0 vid övergång)",
|
||||||
|
"long_description": "Visar minuter tills nästa dyrperiod startar. Under en aktiv period visar detta tiden till perioden EFTER den nuvarande. Returnerar 0 under korta övergångsmoment. Uppdateras varje minut.",
|
||||||
|
"usage_tips": "Förebyggande automation: 'Om next_in_minutes > 0 OCH next_in_minutes < 10, slutför nuvarande laddcykel nu innan priserna ökar'."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ from custom_components.tibber_prices.const import (
|
||||||
VOLATILITY_COLOR_MAPPING,
|
VOLATILITY_COLOR_MAPPING,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Timing sensor color thresholds
|
||||||
|
TIMING_HIGH_PROGRESS_THRESHOLD = 75 # >=75%: High intensity color
|
||||||
|
TIMING_URGENT_THRESHOLD = 15 # <=15 min: Urgent
|
||||||
|
TIMING_SOON_THRESHOLD = 60 # <=60 min: Soon
|
||||||
|
|
||||||
|
|
||||||
def add_icon_color_attribute(
|
def add_icon_color_attribute(
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
|
|
@ -68,6 +73,11 @@ def get_icon_color(
|
||||||
}
|
}
|
||||||
return trend_colors.get(state_value)
|
return trend_colors.get(state_value)
|
||||||
|
|
||||||
|
# Timing sensor colors (best_price = green, peak_price = red/orange)
|
||||||
|
timing_color = get_timing_sensor_color(key, state_value)
|
||||||
|
if timing_color:
|
||||||
|
return timing_color
|
||||||
|
|
||||||
# Price level/rating/volatility colors (based on uppercase value)
|
# Price level/rating/volatility colors (based on uppercase value)
|
||||||
if isinstance(state_value, str):
|
if isinstance(state_value, str):
|
||||||
return (
|
return (
|
||||||
|
|
@ -77,3 +87,38 @@ def get_icon_color(
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_timing_sensor_color(key: str, state_value: Any) -> str | None:
|
||||||
|
"""
|
||||||
|
Get color for best_price/peak_price timing sensors.
|
||||||
|
|
||||||
|
Best price sensors: Green (good for user)
|
||||||
|
Peak price sensors: Red/Orange (warning/alert)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Entity description key
|
||||||
|
state_value: Sensor value (percentage or minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CSS color variable string or None
|
||||||
|
|
||||||
|
"""
|
||||||
|
is_best_price = key.startswith("best_price_")
|
||||||
|
|
||||||
|
if not (is_best_price or key.startswith("peak_price_")):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# No data / zero value
|
||||||
|
if state_value is None or (isinstance(state_value, (int, float)) and state_value == 0):
|
||||||
|
return "var(--disabled-color)"
|
||||||
|
|
||||||
|
# Progress sensors: Intensity based on completion
|
||||||
|
if key.endswith("_progress") and isinstance(state_value, (int, float)):
|
||||||
|
high_intensity = state_value >= TIMING_HIGH_PROGRESS_THRESHOLD
|
||||||
|
if is_best_price:
|
||||||
|
return "var(--success-color)" if high_intensity else "var(--info-color)"
|
||||||
|
return "var(--error-color)" if high_intensity else "var(--warning-color)"
|
||||||
|
|
||||||
|
# All other sensors: Simple period-type color
|
||||||
|
return "var(--success-color)" if is_best_price else "var(--warning-color)"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
|
@ -19,20 +21,38 @@ from custom_components.tibber_prices.sensor.helpers import (
|
||||||
)
|
)
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IconContext:
|
||||||
|
"""Context data for dynamic icon selection."""
|
||||||
|
|
||||||
|
is_on: bool | None = None
|
||||||
|
coordinator_data: dict | None = None
|
||||||
|
has_future_periods_callback: Callable[[], bool] | None = None
|
||||||
|
period_is_active_callback: Callable[[], bool] | None = None
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
# Constants imported from price_utils
|
# Constants imported from price_utils
|
||||||
MINUTES_PER_INTERVAL = 15
|
MINUTES_PER_INTERVAL = 15
|
||||||
|
|
||||||
|
# Timing sensor icon thresholds (in minutes)
|
||||||
|
TIMING_URGENT_THRESHOLD = 15 # ≤15 min: Alert icon
|
||||||
|
TIMING_SOON_THRESHOLD = 60 # ≤1 hour: Timer icon
|
||||||
|
TIMING_MEDIUM_THRESHOLD = 180 # ≤3 hours: Sand timer icon
|
||||||
|
# >3 hours: Outline timer icon
|
||||||
|
|
||||||
|
# Progress sensor constants
|
||||||
|
PROGRESS_MAX = 100 # Maximum progress value (100%)
|
||||||
|
|
||||||
|
|
||||||
def get_dynamic_icon(
|
def get_dynamic_icon(
|
||||||
key: str,
|
key: str,
|
||||||
value: Any,
|
value: Any,
|
||||||
*,
|
*,
|
||||||
is_on: bool | None = None,
|
context: IconContext | None = None,
|
||||||
coordinator_data: dict | None = None,
|
|
||||||
has_future_periods_callback: Callable[[], bool] | None = None,
|
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get dynamic icon based on sensor state.
|
Get dynamic icon based on sensor state.
|
||||||
|
|
@ -42,22 +62,23 @@ def get_dynamic_icon(
|
||||||
Args:
|
Args:
|
||||||
key: Entity description key
|
key: Entity description key
|
||||||
value: Native value of the sensor
|
value: Native value of the sensor
|
||||||
is_on: Binary sensor state (None for regular sensors)
|
context: Optional context with is_on state, coordinator_data, and callbacks
|
||||||
coordinator_data: Coordinator data for price level lookups
|
|
||||||
has_future_periods_callback: Callback to check if future periods exist (binary sensors)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Icon string or None if no dynamic icon applies
|
Icon string or None if no dynamic icon applies
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
ctx = context or IconContext()
|
||||||
|
|
||||||
# Try various icon sources in order
|
# Try various icon sources in order
|
||||||
return (
|
return (
|
||||||
get_trend_icon(key, value)
|
get_trend_icon(key, value)
|
||||||
or get_price_sensor_icon(key, coordinator_data)
|
or get_timing_sensor_icon(key, value, period_is_active_callback=ctx.period_is_active_callback)
|
||||||
|
or get_price_sensor_icon(key, ctx.coordinator_data)
|
||||||
or get_level_sensor_icon(key, value)
|
or get_level_sensor_icon(key, value)
|
||||||
or get_rating_sensor_icon(key, value)
|
or get_rating_sensor_icon(key, value)
|
||||||
or get_volatility_sensor_icon(key, value)
|
or get_volatility_sensor_icon(key, value)
|
||||||
or get_binary_sensor_icon(key, is_on=is_on, has_future_periods_callback=has_future_periods_callback)
|
or get_binary_sensor_icon(key, is_on=ctx.is_on, has_future_periods_callback=ctx.has_future_periods_callback)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -74,6 +95,74 @@ def get_trend_icon(key: str, value: Any) -> str | None:
|
||||||
return trend_icons.get(value)
|
return trend_icons.get(value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_timing_sensor_icon(
|
||||||
|
key: str,
|
||||||
|
value: Any,
|
||||||
|
*,
|
||||||
|
period_is_active_callback: Callable[[], bool] | None = None,
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
Get dynamic icon for best_price/peak_price timing sensors.
|
||||||
|
|
||||||
|
Progress sensors: Different icons based on period state
|
||||||
|
- No period: mdi:help-circle-outline (Unknown/gray)
|
||||||
|
- Waiting (0%, period not active): mdi:timer-pause-outline (paused/waiting)
|
||||||
|
- Active (0%, period running): mdi:circle-outline (just started)
|
||||||
|
- Progress 1-99%: mdi:circle-slice-1 to mdi:circle-slice-7
|
||||||
|
- Complete (100%): mdi:circle-slice-8
|
||||||
|
|
||||||
|
Remaining/Next-in sensors: Different timer icons based on time remaining
|
||||||
|
Timestamp sensors: Static icons (handled by entity description)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Entity description key
|
||||||
|
value: Sensor value (percentage for progress, minutes for countdown)
|
||||||
|
period_is_active_callback: Callback to check if related period is currently active
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Icon string or None if not a timing sensor with dynamic icon
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Unknown state: Show help icon for all timing sensors
|
||||||
|
if value is None and key.startswith(("best_price_", "peak_price_")):
|
||||||
|
return "mdi:help-circle-outline"
|
||||||
|
|
||||||
|
# Progress sensors: Circle-slice icons for visual progress indication
|
||||||
|
# mdi:circle-slice-N where N represents filled portions (1=12.5%, 8=100%)
|
||||||
|
if key.endswith("_progress") and isinstance(value, (int, float)):
|
||||||
|
# Special handling for 0%: Distinguish between waiting and active
|
||||||
|
if value <= 0:
|
||||||
|
# Check if period is currently active via callback
|
||||||
|
is_active = (
|
||||||
|
period_is_active_callback()
|
||||||
|
if (period_is_active_callback and callable(period_is_active_callback))
|
||||||
|
else True
|
||||||
|
)
|
||||||
|
# Period just started (0% but running) vs waiting for next
|
||||||
|
return "mdi:circle-outline" if is_active else "mdi:timer-pause-outline"
|
||||||
|
|
||||||
|
# Calculate slice based on progress percentage
|
||||||
|
slice_num = 8 if value >= PROGRESS_MAX else min(7, max(1, int((value / PROGRESS_MAX) * 8)))
|
||||||
|
return f"mdi:circle-slice-{slice_num}"
|
||||||
|
|
||||||
|
# Remaining/Next-in minutes sensors: Timer icons based on urgency thresholds
|
||||||
|
if key.endswith(("_remaining_minutes", "_next_in_minutes")) and isinstance(value, (int, float)):
|
||||||
|
# Map time remaining to appropriate timer icon
|
||||||
|
urgency_map = [
|
||||||
|
(0, "mdi:timer-off-outline"), # Exactly 0 minutes
|
||||||
|
(TIMING_URGENT_THRESHOLD, "mdi:timer-alert"), # < 15 min: urgent
|
||||||
|
(TIMING_SOON_THRESHOLD, "mdi:timer"), # < 60 min: soon
|
||||||
|
(TIMING_MEDIUM_THRESHOLD, "mdi:timer-sand"), # < 180 min: medium
|
||||||
|
]
|
||||||
|
for threshold, icon in urgency_map:
|
||||||
|
if value <= threshold:
|
||||||
|
return icon
|
||||||
|
return "mdi:timer-outline" # >= 180 min: far away
|
||||||
|
|
||||||
|
# Timestamp sensors use static icons from entity description
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_price_sensor_icon(key: str, coordinator_data: dict | None) -> str | None:
|
def get_price_sensor_icon(key: str, coordinator_data: dict | None) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get icon for current price sensors (dynamic based on price level).
|
Get icon for current price sensors (dynamic based on price level).
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,36 @@ if TYPE_CHECKING:
|
||||||
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_timing_or_volatility_attributes(
|
||||||
|
attributes: dict,
|
||||||
|
key: str,
|
||||||
|
cached_data: dict,
|
||||||
|
native_value: Any = None,
|
||||||
|
) -> None:
|
||||||
|
"""Add attributes for timing or volatility sensors."""
|
||||||
|
if key.endswith("_volatility"):
|
||||||
|
add_volatility_attributes(attributes=attributes, cached_data=cached_data)
|
||||||
|
else:
|
||||||
|
add_period_timing_attributes(attributes=attributes, key=key, state_value=native_value)
|
||||||
|
|
||||||
|
|
||||||
def build_sensor_attributes(
|
def build_sensor_attributes(
|
||||||
key: str,
|
key: str,
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
|
|
@ -121,8 +151,8 @@ def build_sensor_attributes(
|
||||||
)
|
)
|
||||||
elif key == "price_forecast":
|
elif key == "price_forecast":
|
||||||
add_price_forecast_attributes(attributes=attributes, coordinator=coordinator)
|
add_price_forecast_attributes(attributes=attributes, coordinator=coordinator)
|
||||||
elif key.endswith("_volatility"):
|
elif _is_timing_or_volatility_sensor(key):
|
||||||
add_volatility_attributes(attributes=attributes, cached_data=cached_data)
|
_add_timing_or_volatility_attributes(attributes, key, cached_data, native_value)
|
||||||
|
|
||||||
# For current_interval_price_level, add the original level as attribute
|
# 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:
|
if key == "current_interval_price_level" and cached_data.get("last_price_level") is not None:
|
||||||
|
|
@ -887,3 +917,42 @@ def get_current_interval_data(
|
||||||
price_info = coordinator.data.get("priceInfo", {})
|
price_info = coordinator.data.get("priceInfo", {})
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
return find_price_data_for_interval(price_info, now)
|
return find_price_data_for_interval(price_info, now)
|
||||||
|
|
||||||
|
|
||||||
|
def add_period_timing_attributes(
|
||||||
|
attributes: dict,
|
||||||
|
key: str,
|
||||||
|
state_value: Any = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Add timestamp and icon_color attributes for best_price/peak_price timing sensors.
|
||||||
|
|
||||||
|
The timestamp indicates when the sensor value was calculated:
|
||||||
|
- Quarter-hour sensors (end_time, next_start_time): Timestamp of current 15-min interval
|
||||||
|
- Minute-update sensors (remaining_minutes, progress, next_in_minutes): Current minute with :00 seconds
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attributes: Dictionary to add attributes to
|
||||||
|
key: The sensor entity key (e.g., "best_price_end_time")
|
||||||
|
state_value: Current sensor value for icon_color calculation
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Determine if this is a quarter-hour or minute-update sensor
|
||||||
|
is_quarter_hour_sensor = key.endswith(("_end_time", "_next_start_time"))
|
||||||
|
|
||||||
|
now = dt_util.now()
|
||||||
|
|
||||||
|
if is_quarter_hour_sensor:
|
||||||
|
# Quarter-hour sensors: Use timestamp of current 15-minute interval
|
||||||
|
# Round down to the nearest quarter hour (:00, :15, :30, :45)
|
||||||
|
minute = (now.minute // 15) * 15
|
||||||
|
timestamp = now.replace(minute=minute, second=0, microsecond=0)
|
||||||
|
else:
|
||||||
|
# Minute-update sensors: Use current minute with :00 seconds
|
||||||
|
# This ensures clean timestamps despite timer fluctuations
|
||||||
|
timestamp = now.replace(second=0, microsecond=0)
|
||||||
|
|
||||||
|
attributes["timestamp"] = timestamp.isoformat()
|
||||||
|
|
||||||
|
# Add icon_color for dynamic styling
|
||||||
|
add_icon_color_attribute(attributes, key=key, state_value=state_value)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ from custom_components.tibber_prices.average_utils import (
|
||||||
calculate_current_trailing_min,
|
calculate_current_trailing_min,
|
||||||
calculate_next_n_hours_avg,
|
calculate_next_n_hours_avg,
|
||||||
)
|
)
|
||||||
|
from custom_components.tibber_prices.binary_sensor.attributes import (
|
||||||
|
get_price_intervals_attributes,
|
||||||
|
)
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import (
|
||||||
CONF_EXTENDED_DESCRIPTIONS,
|
CONF_EXTENDED_DESCRIPTIONS,
|
||||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
|
@ -30,12 +33,16 @@ from custom_components.tibber_prices.const import (
|
||||||
format_price_unit_minor,
|
format_price_unit_minor,
|
||||||
get_entity_description,
|
get_entity_description,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.coordinator import TIME_SENSITIVE_ENTITY_KEYS
|
from custom_components.tibber_prices.coordinator import (
|
||||||
|
MINUTE_UPDATE_ENTITY_KEYS,
|
||||||
|
TIME_SENSITIVE_ENTITY_KEYS,
|
||||||
|
)
|
||||||
from custom_components.tibber_prices.entity import TibberPricesEntity
|
from custom_components.tibber_prices.entity import TibberPricesEntity
|
||||||
from custom_components.tibber_prices.entity_utils import (
|
from custom_components.tibber_prices.entity_utils import (
|
||||||
add_icon_color_attribute,
|
add_icon_color_attribute,
|
||||||
get_dynamic_icon,
|
get_dynamic_icon,
|
||||||
)
|
)
|
||||||
|
from custom_components.tibber_prices.entity_utils.icons import IconContext
|
||||||
from custom_components.tibber_prices.price_utils import (
|
from custom_components.tibber_prices.price_utils import (
|
||||||
MINUTES_PER_INTERVAL,
|
MINUTES_PER_INTERVAL,
|
||||||
calculate_price_trend,
|
calculate_price_trend,
|
||||||
|
|
@ -76,6 +83,7 @@ LAST_HOUR_OF_DAY = 23
|
||||||
INTERVALS_PER_HOUR = 4 # 15-minute intervals
|
INTERVALS_PER_HOUR = 4 # 15-minute intervals
|
||||||
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
||||||
MIN_HOURS_FOR_LATER_HALF = 3 # Minimum hours needed to calculate later half average
|
MIN_HOURS_FOR_LATER_HALF = 3 # Minimum hours needed to calculate later half average
|
||||||
|
PROGRESS_GRACE_PERIOD_SECONDS = 60 # Show 100% for 1 minute after period ends
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
|
|
@ -93,6 +101,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._attr_has_entity_name = True
|
self._attr_has_entity_name = True
|
||||||
self._value_getter: Callable | None = self._get_value_getter()
|
self._value_getter: Callable | None = self._get_value_getter()
|
||||||
self._time_sensitive_remove_listener: Callable | None = None
|
self._time_sensitive_remove_listener: Callable | None = None
|
||||||
|
self._minute_update_remove_listener: Callable | None = None
|
||||||
self._trend_attributes: dict[str, Any] = {} # Sensor-specific trend attributes
|
self._trend_attributes: dict[str, Any] = {} # Sensor-specific trend attributes
|
||||||
self._cached_trend_value: str | None = None # Cache for trend state
|
self._cached_trend_value: str | None = None # Cache for trend state
|
||||||
|
|
||||||
|
|
@ -106,6 +115,12 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._handle_time_sensitive_update
|
self._handle_time_sensitive_update
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Register with coordinator for minute-by-minute updates if applicable
|
||||||
|
if self.entity_description.key in MINUTE_UPDATE_ENTITY_KEYS:
|
||||||
|
self._minute_update_remove_listener = self.coordinator.async_add_minute_update_listener(
|
||||||
|
self._handle_minute_update
|
||||||
|
)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""When entity will be removed from hass."""
|
"""When entity will be removed from hass."""
|
||||||
await super().async_will_remove_from_hass()
|
await super().async_will_remove_from_hass()
|
||||||
|
|
@ -115,6 +130,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._time_sensitive_remove_listener()
|
self._time_sensitive_remove_listener()
|
||||||
self._time_sensitive_remove_listener = None
|
self._time_sensitive_remove_listener = None
|
||||||
|
|
||||||
|
# Remove minute-update listener if registered
|
||||||
|
if self._minute_update_remove_listener:
|
||||||
|
self._minute_update_remove_listener()
|
||||||
|
self._minute_update_remove_listener = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_time_sensitive_update(self) -> None:
|
def _handle_time_sensitive_update(self) -> None:
|
||||||
"""Handle time-sensitive update from coordinator."""
|
"""Handle time-sensitive update from coordinator."""
|
||||||
|
|
@ -124,6 +144,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._trend_attributes = {}
|
self._trend_attributes = {}
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_minute_update(self) -> None:
|
||||||
|
"""Handle minute-by-minute update from coordinator."""
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
|
|
@ -247,6 +272,41 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
"tomorrow_volatility": lambda: self._get_volatility_value(volatility_type="tomorrow"),
|
"tomorrow_volatility": lambda: self._get_volatility_value(volatility_type="tomorrow"),
|
||||||
"next_24h_volatility": lambda: self._get_volatility_value(volatility_type="next_24h"),
|
"next_24h_volatility": lambda: self._get_volatility_value(volatility_type="next_24h"),
|
||||||
"today_tomorrow_volatility": lambda: self._get_volatility_value(volatility_type="today_tomorrow"),
|
"today_tomorrow_volatility": lambda: self._get_volatility_value(volatility_type="today_tomorrow"),
|
||||||
|
# ================================================================
|
||||||
|
# BEST/PEAK PRICE TIMING SENSORS (period-based time tracking)
|
||||||
|
# ================================================================
|
||||||
|
# Best Price timing sensors
|
||||||
|
"best_price_end_time": lambda: self._get_period_timing_value(
|
||||||
|
period_type="best_price", value_type="end_time"
|
||||||
|
),
|
||||||
|
"best_price_remaining_minutes": lambda: self._get_period_timing_value(
|
||||||
|
period_type="best_price", value_type="remaining_minutes"
|
||||||
|
),
|
||||||
|
"best_price_progress": lambda: self._get_period_timing_value(
|
||||||
|
period_type="best_price", value_type="progress"
|
||||||
|
),
|
||||||
|
"best_price_next_start_time": lambda: self._get_period_timing_value(
|
||||||
|
period_type="best_price", value_type="next_start_time"
|
||||||
|
),
|
||||||
|
"best_price_next_in_minutes": lambda: self._get_period_timing_value(
|
||||||
|
period_type="best_price", value_type="next_in_minutes"
|
||||||
|
),
|
||||||
|
# Peak Price timing sensors
|
||||||
|
"peak_price_end_time": lambda: self._get_period_timing_value(
|
||||||
|
period_type="peak_price", value_type="end_time"
|
||||||
|
),
|
||||||
|
"peak_price_remaining_minutes": lambda: self._get_period_timing_value(
|
||||||
|
period_type="peak_price", value_type="remaining_minutes"
|
||||||
|
),
|
||||||
|
"peak_price_progress": lambda: self._get_period_timing_value(
|
||||||
|
period_type="peak_price", value_type="progress"
|
||||||
|
),
|
||||||
|
"peak_price_next_start_time": lambda: self._get_period_timing_value(
|
||||||
|
period_type="peak_price", value_type="next_start_time"
|
||||||
|
),
|
||||||
|
"peak_price_next_in_minutes": lambda: self._get_period_timing_value(
|
||||||
|
period_type="peak_price", value_type="next_in_minutes"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return handlers.get(key)
|
return handlers.get(key)
|
||||||
|
|
@ -930,6 +990,188 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
# Return lowercase for ENUM device class
|
# Return lowercase for ENUM device class
|
||||||
return volatility.lower()
|
return volatility.lower()
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# BEST/PEAK PRICE TIMING METHODS (period-based time tracking)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
def _get_period_timing_value(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
period_type: str,
|
||||||
|
value_type: str,
|
||||||
|
) -> datetime | float | None:
|
||||||
|
"""
|
||||||
|
Get timing-related values for best_price/peak_price periods.
|
||||||
|
|
||||||
|
This method provides timing information based on whether a period is currently
|
||||||
|
active or not, ensuring sensors always provide useful information.
|
||||||
|
|
||||||
|
Value types behavior:
|
||||||
|
- end_time: Active period → current end | No active → next period end | None if no periods
|
||||||
|
- next_start_time: Active period → next-next start | No active → next start | None if no more
|
||||||
|
- remaining_minutes: Active period → minutes to end | No active → 0
|
||||||
|
- progress: Active period → 0-100% | No active → 0
|
||||||
|
- next_in_minutes: Active period → minutes to next-next | No active → minutes to next | None if no more
|
||||||
|
|
||||||
|
Args:
|
||||||
|
period_type: "best_price" or "peak_price"
|
||||||
|
value_type: "end_time", "remaining_minutes", "progress", "next_start_time", "next_in_minutes"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- datetime for end_time/next_start_time
|
||||||
|
- float for remaining_minutes/next_in_minutes/progress (or 0 when not active)
|
||||||
|
- None if no relevant period data available
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get period data from coordinator
|
||||||
|
periods_data = self.coordinator.data.get("periods", {})
|
||||||
|
period_data = periods_data.get(period_type)
|
||||||
|
|
||||||
|
if not period_data or not period_data.get("periods"):
|
||||||
|
# No periods available - return 0 for numeric sensors, None for timestamps
|
||||||
|
return 0 if value_type in ("remaining_minutes", "progress", "next_in_minutes") else None
|
||||||
|
|
||||||
|
period_summaries = period_data["periods"]
|
||||||
|
now = dt_util.now()
|
||||||
|
|
||||||
|
# Find current, previous and next periods
|
||||||
|
current_period = self._find_active_period(period_summaries, now)
|
||||||
|
previous_period = self._find_previous_period(period_summaries, now)
|
||||||
|
next_period = self._find_next_period(period_summaries, now, skip_current=bool(current_period))
|
||||||
|
|
||||||
|
# Delegate to specific calculators
|
||||||
|
return self._calculate_timing_value(value_type, current_period, previous_period, next_period, now)
|
||||||
|
|
||||||
|
def _calculate_timing_value(
|
||||||
|
self,
|
||||||
|
value_type: str,
|
||||||
|
current_period: dict | None,
|
||||||
|
previous_period: dict | None,
|
||||||
|
next_period: dict | None,
|
||||||
|
now: datetime,
|
||||||
|
) -> datetime | float | None:
|
||||||
|
"""Calculate specific timing value based on type and available periods."""
|
||||||
|
# Define calculation strategies for each value type
|
||||||
|
calculators = {
|
||||||
|
"end_time": lambda: (
|
||||||
|
current_period.get("end") if current_period else (next_period.get("end") if next_period else None)
|
||||||
|
),
|
||||||
|
"next_start_time": lambda: next_period.get("start") if next_period else None,
|
||||||
|
"remaining_minutes": lambda: (self._calc_remaining_minutes(current_period, now) if current_period else 0),
|
||||||
|
"progress": lambda: self._calc_progress_with_grace_period(current_period, previous_period, now),
|
||||||
|
"next_in_minutes": lambda: (self._calc_next_in_minutes(next_period, now) if next_period else None),
|
||||||
|
}
|
||||||
|
|
||||||
|
calculator = calculators.get(value_type)
|
||||||
|
return calculator() if calculator else None
|
||||||
|
|
||||||
|
def _find_active_period(self, periods: list, now: datetime) -> dict | None:
|
||||||
|
"""Find currently active period."""
|
||||||
|
for period in periods:
|
||||||
|
start = period.get("start")
|
||||||
|
end = period.get("end")
|
||||||
|
if start and end and start <= now < end:
|
||||||
|
return period
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_previous_period(self, periods: list, now: datetime) -> dict | None:
|
||||||
|
"""Find the most recent period that has already ended."""
|
||||||
|
past_periods = [p for p in periods if p.get("end") and p.get("end") <= now]
|
||||||
|
|
||||||
|
if not past_periods:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sort by end time descending to get the most recent one
|
||||||
|
past_periods.sort(key=lambda p: p["end"], reverse=True)
|
||||||
|
return past_periods[0]
|
||||||
|
|
||||||
|
def _find_next_period(self, periods: list, now: datetime, *, skip_current: bool = False) -> dict | None:
|
||||||
|
"""
|
||||||
|
Find next future period.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
periods: List of period dictionaries
|
||||||
|
now: Current time
|
||||||
|
skip_current: If True, skip the first future period (to get next-next)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Next period dict or None if no future periods
|
||||||
|
|
||||||
|
"""
|
||||||
|
future_periods = [p for p in periods if p.get("start") and p.get("start") > now]
|
||||||
|
|
||||||
|
if not future_periods:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sort by start time to ensure correct order
|
||||||
|
future_periods.sort(key=lambda p: p["start"])
|
||||||
|
|
||||||
|
# Return second period if skip_current=True (next-next), otherwise first (next)
|
||||||
|
if skip_current and len(future_periods) > 1:
|
||||||
|
return future_periods[1]
|
||||||
|
if not skip_current and future_periods:
|
||||||
|
return future_periods[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _calc_remaining_minutes(self, period: dict, now: datetime) -> float:
|
||||||
|
"""Calculate minutes until period ends."""
|
||||||
|
end = period.get("end")
|
||||||
|
if not end:
|
||||||
|
return 0
|
||||||
|
delta = end - now
|
||||||
|
return max(0, delta.total_seconds() / 60)
|
||||||
|
|
||||||
|
def _calc_next_in_minutes(self, period: dict, now: datetime) -> float:
|
||||||
|
"""Calculate minutes until period starts."""
|
||||||
|
start = period.get("start")
|
||||||
|
if not start:
|
||||||
|
return 0
|
||||||
|
delta = start - now
|
||||||
|
return max(0, delta.total_seconds() / 60)
|
||||||
|
|
||||||
|
def _calc_progress(self, period: dict, now: datetime) -> float:
|
||||||
|
"""Calculate progress percentage (0-100) of current period."""
|
||||||
|
start = period.get("start")
|
||||||
|
end = period.get("end")
|
||||||
|
if not start or not end:
|
||||||
|
return 0
|
||||||
|
total_duration = (end - start).total_seconds()
|
||||||
|
if total_duration <= 0:
|
||||||
|
return 0
|
||||||
|
elapsed = (now - start).total_seconds()
|
||||||
|
progress = (elapsed / total_duration) * 100
|
||||||
|
return min(100, max(0, progress))
|
||||||
|
|
||||||
|
def _calc_progress_with_grace_period(
|
||||||
|
self, current_period: dict | None, previous_period: dict | None, now: datetime
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate progress with grace period after period end.
|
||||||
|
|
||||||
|
Shows 100% for 1 minute after period ends to allow triggers on 100% completion.
|
||||||
|
This prevents the progress from jumping directly from ~99% to 0% without ever
|
||||||
|
reaching 100%, which would make automations like "when progress = 100%" impossible.
|
||||||
|
"""
|
||||||
|
# If we have an active period, calculate normal progress
|
||||||
|
if current_period:
|
||||||
|
return self._calc_progress(current_period, now)
|
||||||
|
|
||||||
|
# No active period - check if we just finished one (within grace period)
|
||||||
|
if previous_period:
|
||||||
|
previous_end = previous_period.get("end")
|
||||||
|
if previous_end:
|
||||||
|
seconds_since_end = (now - previous_end).total_seconds()
|
||||||
|
# Grace period: Show 100% for defined time after period ended
|
||||||
|
if 0 <= seconds_since_end <= PROGRESS_GRACE_PERIOD_SECONDS:
|
||||||
|
return 100
|
||||||
|
|
||||||
|
# No active period and either no previous period or grace period expired
|
||||||
|
return 0
|
||||||
|
|
||||||
# Add method to get future price intervals
|
# Add method to get future price intervals
|
||||||
def _get_price_forecast_value(self) -> str | None:
|
def _get_price_forecast_value(self) -> str | None:
|
||||||
"""Get the highest or lowest price status for the price forecast entity."""
|
"""Get the highest or lowest price status for the price forecast entity."""
|
||||||
|
|
@ -962,16 +1204,45 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_unit_of_measurement(self) -> str | None:
|
def native_unit_of_measurement(self) -> str | None:
|
||||||
"""Return the unit of measurement dynamically based on currency."""
|
"""Return the unit of measurement dynamically based on currency or entity description."""
|
||||||
if self.entity_description.device_class != SensorDeviceClass.MONETARY:
|
# For MONETARY sensors, return currency-specific unit
|
||||||
return None
|
if self.entity_description.device_class == SensorDeviceClass.MONETARY:
|
||||||
|
currency = None
|
||||||
|
if self.coordinator.data:
|
||||||
|
price_info = self.coordinator.data.get("priceInfo", {})
|
||||||
|
currency = price_info.get("currency")
|
||||||
|
return format_price_unit_minor(currency)
|
||||||
|
|
||||||
currency = None
|
# For all other sensors, use unit from entity description
|
||||||
if self.coordinator.data:
|
return self.entity_description.native_unit_of_measurement
|
||||||
price_info = self.coordinator.data.get("priceInfo", {})
|
|
||||||
currency = price_info.get("currency")
|
|
||||||
|
|
||||||
return format_price_unit_minor(currency)
|
def _is_best_price_period_active(self) -> bool:
|
||||||
|
"""Check if the current time is within a best price period."""
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return False
|
||||||
|
attrs = get_price_intervals_attributes(self.coordinator.data, reverse_sort=False)
|
||||||
|
if not attrs:
|
||||||
|
return False
|
||||||
|
start = attrs.get("start")
|
||||||
|
end = attrs.get("end")
|
||||||
|
if not start or not end:
|
||||||
|
return False
|
||||||
|
now = dt_util.now()
|
||||||
|
return start <= now < end
|
||||||
|
|
||||||
|
def _is_peak_price_period_active(self) -> bool:
|
||||||
|
"""Check if the current time is within a peak price period."""
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return False
|
||||||
|
attrs = get_price_intervals_attributes(self.coordinator.data, reverse_sort=True)
|
||||||
|
if not attrs:
|
||||||
|
return False
|
||||||
|
start = attrs.get("start")
|
||||||
|
end = attrs.get("end")
|
||||||
|
if not start or not end:
|
||||||
|
return False
|
||||||
|
now = dt_util.now()
|
||||||
|
return start <= now < end
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None:
|
def icon(self) -> str | None:
|
||||||
|
|
@ -979,11 +1250,21 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
key = self.entity_description.key
|
key = self.entity_description.key
|
||||||
value = self.native_value
|
value = self.native_value
|
||||||
|
|
||||||
# Use centralized icon logic
|
# Create callback for period active state check (used by timing sensors)
|
||||||
|
period_is_active_callback = None
|
||||||
|
if key.startswith("best_price_"):
|
||||||
|
period_is_active_callback = self._is_best_price_period_active
|
||||||
|
elif key.startswith("peak_price_"):
|
||||||
|
period_is_active_callback = self._is_peak_price_period_active
|
||||||
|
|
||||||
|
# Use centralized icon logic with context
|
||||||
icon = get_dynamic_icon(
|
icon = get_dynamic_icon(
|
||||||
key=key,
|
key=key,
|
||||||
value=value,
|
value=value,
|
||||||
coordinator_data=self.coordinator.data,
|
context=IconContext(
|
||||||
|
coordinator_data=self.coordinator.data,
|
||||||
|
period_is_active_callback=period_is_active_callback,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fall back to static icon from entity description
|
# Fall back to static icon from entity description
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ Organization by calculation pattern:
|
||||||
4. 24h windows: Trailing/leading statistics
|
4. 24h windows: Trailing/leading statistics
|
||||||
5. Future forecast: N-hour windows from next interval
|
5. Future forecast: N-hour windows from next interval
|
||||||
6. Volatility: Price variation analysis
|
6. Volatility: Price variation analysis
|
||||||
7. Diagnostic: System metadata
|
7. Best/Peak Price timing: Period-based time tracking
|
||||||
|
8. Diagnostic: System metadata
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -21,7 +22,7 @@ from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SENSOR DEFINITIONS - Grouped by calculation method
|
# SENSOR DEFINITIONS - Grouped by calculation method
|
||||||
|
|
@ -38,7 +39,8 @@ from homeassistant.const import EntityCategory
|
||||||
# 4. 24h windows: Trailing/leading from current interval
|
# 4. 24h windows: Trailing/leading from current interval
|
||||||
# 5. Future forecast: N-hour windows starting from next interval
|
# 5. Future forecast: N-hour windows starting from next interval
|
||||||
# 6. Volatility: Statistical analysis of price variation
|
# 6. Volatility: Statistical analysis of price variation
|
||||||
# 7. Diagnostic: System information and metadata
|
# 7. Best/Peak Price timing: Period-based time tracking (requires minute updates)
|
||||||
|
# 8. Diagnostic: System information and metadata
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
@ -594,7 +596,105 @@ VOLATILITY_SENSORS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# 7. DIAGNOSTIC SENSORS (data availability and metadata)
|
# 7. BEST/PEAK PRICE TIMING SENSORS (period-based time tracking)
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# These sensors track time relative to best_price/peak_price binary sensor periods.
|
||||||
|
# They require minute-by-minute updates via async_track_time_interval.
|
||||||
|
#
|
||||||
|
# When period is active (binary_sensor ON):
|
||||||
|
# - end_time: Timestamp when current period ends
|
||||||
|
# - remaining_minutes: Minutes until period ends
|
||||||
|
# - progress: Percentage of period completed (0-100%)
|
||||||
|
#
|
||||||
|
# When period is inactive (binary_sensor OFF):
|
||||||
|
# - next_start_time: Timestamp when next period starts
|
||||||
|
# - next_in_minutes: Minutes until next period starts
|
||||||
|
#
|
||||||
|
# All return None/Unknown when no period is active/scheduled.
|
||||||
|
|
||||||
|
BEST_PRICE_TIMING_SENSORS = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="best_price_end_time",
|
||||||
|
translation_key="best_price_end_time",
|
||||||
|
name="Best Price Period End",
|
||||||
|
icon="mdi:clock-end",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="best_price_remaining_minutes",
|
||||||
|
translation_key="best_price_remaining_minutes",
|
||||||
|
name="Best Price Remaining Time",
|
||||||
|
icon="mdi:timer-sand",
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="best_price_progress",
|
||||||
|
translation_key="best_price_progress",
|
||||||
|
name="Best Price Progress",
|
||||||
|
icon="mdi:percent", # Dynamic: mdi:percent-0 to mdi:percent-100
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="best_price_next_start_time",
|
||||||
|
translation_key="best_price_next_start_time",
|
||||||
|
name="Best Price Next Period Start",
|
||||||
|
icon="mdi:clock-start",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="best_price_next_in_minutes",
|
||||||
|
translation_key="best_price_next_in_minutes",
|
||||||
|
name="Best Price Starts In",
|
||||||
|
icon="mdi:timer-outline",
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
PEAK_PRICE_TIMING_SENSORS = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="peak_price_end_time",
|
||||||
|
translation_key="peak_price_end_time",
|
||||||
|
name="Peak Price Period End",
|
||||||
|
icon="mdi:clock-end",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="peak_price_remaining_minutes",
|
||||||
|
translation_key="peak_price_remaining_minutes",
|
||||||
|
name="Peak Price Remaining Time",
|
||||||
|
icon="mdi:timer-sand",
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="peak_price_progress",
|
||||||
|
translation_key="peak_price_progress",
|
||||||
|
name="Peak Price Progress",
|
||||||
|
icon="mdi:percent", # Dynamic: mdi:percent-0 to mdi:percent-100
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="peak_price_next_start_time",
|
||||||
|
translation_key="peak_price_next_start_time",
|
||||||
|
name="Peak Price Next Period Start",
|
||||||
|
icon="mdi:clock-start",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="peak_price_next_in_minutes",
|
||||||
|
translation_key="peak_price_next_in_minutes",
|
||||||
|
name="Peak Price Starts In",
|
||||||
|
icon="mdi:timer-outline",
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 8. DIAGNOSTIC SENSORS (data availability and metadata)
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
DIAGNOSTIC_SENSORS = (
|
DIAGNOSTIC_SENSORS = (
|
||||||
|
|
@ -633,5 +733,7 @@ ENTITY_DESCRIPTIONS = (
|
||||||
*FUTURE_AVG_SENSORS,
|
*FUTURE_AVG_SENSORS,
|
||||||
*FUTURE_TREND_SENSORS,
|
*FUTURE_TREND_SENSORS,
|
||||||
*VOLATILITY_SENSORS,
|
*VOLATILITY_SENSORS,
|
||||||
|
*BEST_PRICE_TIMING_SENSORS,
|
||||||
|
*PEAK_PRICE_TIMING_SENSORS,
|
||||||
*DIAGNOSTIC_SENSORS,
|
*DIAGNOSTIC_SENSORS,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -511,6 +511,36 @@
|
||||||
"very_high": "Sehr hoch"
|
"very_high": "Sehr hoch"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"name": "Günstige Periode endet"
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"name": "Günstige Periode verbleibend"
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"name": "Günstige Periode Fortschritt"
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"name": "Nächste günstige Periode startet"
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"name": "Günstige Periode startet in"
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"name": "Teure Periode endet"
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"name": "Teure Periode verbleibend"
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"name": "Teure Periode Fortschritt"
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"name": "Nächste teure Periode startet"
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"name": "Teure Periode startet in"
|
||||||
|
},
|
||||||
"price_forecast": {
|
"price_forecast": {
|
||||||
"name": "Preisprognose"
|
"name": "Preisprognose"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,36 @@
|
||||||
"very_high": "Very High"
|
"very_high": "Very High"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"name": "Best Price Period End"
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"name": "Best Price Remaining Time"
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"name": "Best Price Progress"
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"name": "Best Price Next Period Start"
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"name": "Best Price Starts In"
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"name": "Peak Price Period End"
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"name": "Peak Price Remaining Time"
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"name": "Peak Price Progress"
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"name": "Peak Price Next Period Start"
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"name": "Peak Price Starts In"
|
||||||
|
},
|
||||||
"price_forecast": {
|
"price_forecast": {
|
||||||
"name": "Price Forecast"
|
"name": "Price Forecast"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,36 @@
|
||||||
"very_high": "Svært Høy"
|
"very_high": "Svært Høy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"name": "Beste prisperiode slutter"
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"name": "Beste prisperiode gjenværende tid"
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"name": "Beste prisperiode fremgang"
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"name": "Neste beste prisperiode starter"
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"name": "Beste prisperiode starter om"
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"name": "Topprisperiode slutter"
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"name": "Topprisperiode gjenværende tid"
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"name": "Topprisperiode fremgang"
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"name": "Neste topprisperiode starter"
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"name": "Topprisperiode starter om"
|
||||||
|
},
|
||||||
"price_forecast": {
|
"price_forecast": {
|
||||||
"name": "Prisprognose"
|
"name": "Prisprognose"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,36 @@
|
||||||
"very_high": "Zeer Hoog"
|
"very_high": "Zeer Hoog"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"name": "Beste prijsperiode eindigt"
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"name": "Beste prijsperiode resterende tijd"
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"name": "Beste prijsperiode voortgang"
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"name": "Volgende beste prijsperiode start"
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"name": "Beste prijsperiode start over"
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"name": "Piekprijsperiode eindigt"
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"name": "Piekprijsperiode resterende tijd"
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"name": "Piekprijsperiode voortgang"
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"name": "Volgende piekprijsperiode start"
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"name": "Piekprijsperiode start over"
|
||||||
|
},
|
||||||
"price_forecast": {
|
"price_forecast": {
|
||||||
"name": "Prijsprognose"
|
"name": "Prijsprognose"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,36 @@
|
||||||
"very_high": "Mycket Hög"
|
"very_high": "Mycket Hög"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"best_price_end_time": {
|
||||||
|
"name": "Bästa prisperiod slutar"
|
||||||
|
},
|
||||||
|
"best_price_remaining_minutes": {
|
||||||
|
"name": "Bästa prisperiod återstående tid"
|
||||||
|
},
|
||||||
|
"best_price_progress": {
|
||||||
|
"name": "Bästa prisperiod framsteg"
|
||||||
|
},
|
||||||
|
"best_price_next_start_time": {
|
||||||
|
"name": "Nästa bästa prisperiod startar"
|
||||||
|
},
|
||||||
|
"best_price_next_in_minutes": {
|
||||||
|
"name": "Bästa prisperiod startar om"
|
||||||
|
},
|
||||||
|
"peak_price_end_time": {
|
||||||
|
"name": "Topprisperiod slutar"
|
||||||
|
},
|
||||||
|
"peak_price_remaining_minutes": {
|
||||||
|
"name": "Topprisperiod återstående tid"
|
||||||
|
},
|
||||||
|
"peak_price_progress": {
|
||||||
|
"name": "Topprisperiod framsteg"
|
||||||
|
},
|
||||||
|
"peak_price_next_start_time": {
|
||||||
|
"name": "Nästa topprisperiod startar"
|
||||||
|
},
|
||||||
|
"peak_price_next_in_minutes": {
|
||||||
|
"name": "Topprisperiod startar om"
|
||||||
|
},
|
||||||
"price_forecast": {
|
"price_forecast": {
|
||||||
"name": "Prisprognos"
|
"name": "Prisprognos"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue