mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
feat(sensor): add next/previous/rolling-hour price rank sensors
Rename the three existing price rank sensors from price_rank_* to
current_interval_price_rank_* to clarify they rank the current
quarter-hour interval's price, not a daily aggregate — consistent with
current_interval_price_level / current_interval_price_rating naming.
Add 8 new rank sensors covering additional subjects and reference windows:
- next_interval_price_rank_{today,today_tomorrow}
- previous_interval_price_rank_{today,today_tomorrow}
- current_hour_price_rank_{today,today_tomorrow} (5-interval rolling avg)
- next_hour_price_rank_{today,today_tomorrow} (5-interval rolling avg)
All new sensors are disabled by default. The volatility calculator gains a
subject parameter (_get_subject_price / _get_subject_price_attr_key /
_get_rolling_hour_avg_price) to select which price to rank. Sensor key
routing in value_getters.py and attributes/__init__.py updated accordingly.
No migration entries needed — the original price_rank_* sensors were never
released to users.
All 5 translation files updated. sensor-reference.md regenerated (129 entities).
Impact: Users can now track price rank for the next interval (look-ahead),
the previous interval (logging), and rolling hourly averages — for both
same-day and two-day reference windows.
This commit is contained in:
parent
dd59c687e3
commit
51a62d712f
17 changed files with 3915 additions and 3319 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -191,7 +191,7 @@ def build_sensor_attributes( # noqa: PLR0912
|
|||
elif _is_timing_or_volatility_sensor(key):
|
||||
_add_timing_or_volatility_attributes(attributes, key, cached_data, native_value, time=time)
|
||||
|
||||
elif key in ("price_rank_today", "price_rank_tomorrow", "price_rank_today_tomorrow"):
|
||||
elif "_price_rank_" in key:
|
||||
add_percentile_rank_attributes(attributes, cached_data, time=time)
|
||||
|
||||
elif key in ("day_pattern_yesterday", "day_pattern_today", "day_pattern_tomorrow"):
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ from custom_components.tibber_prices.const import (
|
|||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||
get_display_unit_factor,
|
||||
)
|
||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
||||
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
|
||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute, find_rolling_hour_center_index
|
||||
from custom_components.tibber_prices.sensor.attributes import (
|
||||
add_volatility_type_attributes,
|
||||
get_prices_for_volatility,
|
||||
|
|
@ -159,18 +160,25 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
|
|||
"""
|
||||
return self._last_volatility_attributes
|
||||
|
||||
def get_percentile_rank_value(self, *, percentile_type: str) -> float | None:
|
||||
def get_percentile_rank_value(
|
||||
self,
|
||||
*,
|
||||
percentile_type: str,
|
||||
subject: str = "current_interval",
|
||||
) -> float | None:
|
||||
"""
|
||||
Calculate the percentile rank of the current price within a reference set.
|
||||
Calculate the percentile rank of a subject price within a reference set.
|
||||
|
||||
The result is 0-100: percentage of reference prices strictly cheaper than
|
||||
the current interval price. 0% = cheapest, ~99% = most expensive.
|
||||
the subject price. 0% = cheapest, ~99% = most expensive.
|
||||
|
||||
Also stores detailed attributes in self._last_percentile_rank_attributes
|
||||
for use in extra_state_attributes.
|
||||
|
||||
Args:
|
||||
percentile_type: One of "today", "tomorrow", "today_tomorrow".
|
||||
percentile_type: Reference window - one of "today", "tomorrow", "today_tomorrow".
|
||||
subject: Price to rank - one of "current_interval" (default), "next_interval",
|
||||
"previous_interval", "current_hour", "next_hour".
|
||||
|
||||
Returns:
|
||||
Percentile rank (0.0-100.0) or None if unavailable.
|
||||
|
|
@ -179,14 +187,10 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
|
|||
if not self.has_data():
|
||||
return None
|
||||
|
||||
# Get current interval price
|
||||
current_interval = self.coordinator.get_current_interval()
|
||||
if current_interval is None:
|
||||
# Get the price of the subject to rank
|
||||
subject_price = self._get_subject_price(subject)
|
||||
if subject_price is None:
|
||||
return None
|
||||
current_price_raw = current_interval.get("total")
|
||||
if current_price_raw is None:
|
||||
return None
|
||||
current_price = float(current_price_raw)
|
||||
|
||||
# Get reference prices for this type (reuse volatility helper)
|
||||
reference_prices = get_prices_for_volatility(
|
||||
|
|
@ -198,16 +202,17 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
|
|||
return None
|
||||
|
||||
# Calculate percentile rank
|
||||
rank = calculate_percentile_rank(current_price, reference_prices)
|
||||
rank = calculate_percentile_rank(subject_price, reference_prices)
|
||||
if rank is None:
|
||||
return None
|
||||
|
||||
# Convert to display units for attribute storage
|
||||
factor = get_display_unit_factor(self.config_entry)
|
||||
price_attr_key = self._get_subject_price_attr_key(subject)
|
||||
|
||||
self._last_percentile_rank_attributes = {
|
||||
"current_price": round(current_price * factor, 2),
|
||||
"prices_below_count": bisect.bisect_left(sorted(reference_prices), current_price),
|
||||
price_attr_key: round(subject_price * factor, 2),
|
||||
"prices_below_count": bisect.bisect_left(sorted(reference_prices), subject_price),
|
||||
"interval_count": len(reference_prices),
|
||||
"reference_min": round(min(reference_prices) * factor, 2),
|
||||
"reference_max": round(max(reference_prices) * factor, 2),
|
||||
|
|
@ -216,6 +221,78 @@ class TibberPricesVolatilityCalculator(TibberPricesBaseCalculator):
|
|||
|
||||
return rank
|
||||
|
||||
def _get_subject_price(self, subject: str) -> float | None:
|
||||
"""
|
||||
Get the price of the subject to rank.
|
||||
|
||||
Args:
|
||||
subject: One of "current_interval", "next_interval", "previous_interval",
|
||||
"current_hour", "next_hour".
|
||||
|
||||
Returns:
|
||||
Price as float or None if unavailable.
|
||||
|
||||
"""
|
||||
if subject == "current_interval":
|
||||
interval = self.find_interval_at_offset(0)
|
||||
elif subject == "next_interval":
|
||||
interval = self.find_interval_at_offset(1)
|
||||
elif subject == "previous_interval":
|
||||
interval = self.find_interval_at_offset(-1)
|
||||
elif subject in ("current_hour", "next_hour"):
|
||||
hour_offset = 0 if subject == "current_hour" else 1
|
||||
return self._get_rolling_hour_avg_price(hour_offset)
|
||||
else:
|
||||
return None
|
||||
|
||||
if interval is None:
|
||||
return None
|
||||
raw = interval.get("total")
|
||||
return float(raw) if raw is not None else None
|
||||
|
||||
def _get_subject_price_attr_key(self, subject: str) -> str:
|
||||
"""Return the attribute key name for the subject's price."""
|
||||
return {
|
||||
"current_interval": "current_price",
|
||||
"next_interval": "next_price",
|
||||
"previous_interval": "previous_price",
|
||||
"current_hour": "current_hour_avg_price",
|
||||
"next_hour": "next_hour_avg_price",
|
||||
}.get(subject, "ranked_price")
|
||||
|
||||
def _get_rolling_hour_avg_price(self, hour_offset: int) -> float | None:
|
||||
"""
|
||||
Get the rolling 1h average price for the given hour offset.
|
||||
|
||||
Uses the same 5-interval window as current_hour_average_price.
|
||||
|
||||
Args:
|
||||
hour_offset: 0 for current hour, 1 for next hour.
|
||||
|
||||
Returns:
|
||||
Average price as float or None if unavailable.
|
||||
|
||||
"""
|
||||
all_prices = get_intervals_for_day_offsets(self.coordinator_data, [-1, 0, 1])
|
||||
if not all_prices:
|
||||
return None
|
||||
|
||||
time = self.coordinator.time
|
||||
now = time.now()
|
||||
center_idx = find_rolling_hour_center_index(all_prices, now, hour_offset, time=time)
|
||||
if center_idx is None:
|
||||
return None
|
||||
|
||||
window: list[float] = []
|
||||
for offset in range(-2, 3):
|
||||
idx = center_idx + offset
|
||||
if 0 <= idx < len(all_prices):
|
||||
raw = all_prices[idx].get("total")
|
||||
if raw is not None:
|
||||
window.append(float(raw))
|
||||
|
||||
return calculate_mean(window) if window else None
|
||||
|
||||
def get_percentile_rank_attributes(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get stored percentile rank attributes from last calculation.
|
||||
|
|
|
|||
|
|
@ -100,6 +100,22 @@ MIN_HOURS_FOR_LATER_HALF = 3 # Minimum hours needed to calculate later half ave
|
|||
_SENTINEL = object()
|
||||
|
||||
|
||||
def _extract_percentile_rank_type(key: str) -> str | None:
|
||||
"""
|
||||
Extract the reference-window type from a price rank sensor key.
|
||||
|
||||
Returns "today_tomorrow", "tomorrow", or "today" based on the key suffix.
|
||||
Returns None if the key is not a price rank sensor key.
|
||||
"""
|
||||
if "_rank_today_tomorrow" in key:
|
||||
return "today_tomorrow"
|
||||
if "_rank_tomorrow" in key:
|
||||
return "tomorrow"
|
||||
if "_rank_today" in key:
|
||||
return "today"
|
||||
return None
|
||||
|
||||
|
||||
class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
||||
"""tibber_prices Sensor class with state restoration."""
|
||||
|
||||
|
|
@ -1165,7 +1181,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
|
|||
"trend_change_attributes": self._trend_calculator.get_trend_change_attributes(),
|
||||
"volatility_attributes": self._volatility_calculator.get_volatility_attributes(),
|
||||
"percentile_rank_attributes": self._volatility_calculator.get_percentile_rank_attributes(),
|
||||
"percentile_rank_type": key.removeprefix("price_rank_") if key.startswith("price_rank_") else None,
|
||||
"percentile_rank_type": _extract_percentile_rank_type(key),
|
||||
"coordinator_data": self.coordinator.data,
|
||||
"last_extreme_interval": self._daily_stat_calculator.get_last_extreme_interval(),
|
||||
"last_energy_tax_averages": self._daily_stat_calculator.get_last_energy_tax_averages(),
|
||||
|
|
|
|||
|
|
@ -753,21 +753,24 @@ VOLATILITY_SENSORS = (
|
|||
# - today_tomorrow: 192 combined intervals when tomorrow is available
|
||||
#
|
||||
# Use case: "Is now the right time to run a large appliance?"
|
||||
# - price_rank_today < 25 → bottom quartile, great time to use energy
|
||||
# - price_rank_today > 75 → top quartile, consider delaying consumption
|
||||
# - current_interval_price_rank_today < 25 → bottom quartile, great time to use energy
|
||||
# - current_interval_price_rank_today > 75 → top quartile, consider delaying consumption
|
||||
|
||||
PERCENTILE_RANK_SENSORS = (
|
||||
# ----------------------------------------------------------------
|
||||
# Current interval rank sensors
|
||||
# ----------------------------------------------------------------
|
||||
SensorEntityDescription(
|
||||
key="price_rank_today",
|
||||
translation_key="price_rank_today",
|
||||
key="current_interval_price_rank_today",
|
||||
translation_key="current_interval_price_rank_today",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None, # Position metric: no statistics
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_rank_tomorrow",
|
||||
translation_key="price_rank_tomorrow",
|
||||
key="current_interval_price_rank_tomorrow",
|
||||
translation_key="current_interval_price_rank_tomorrow",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None, # Position metric: no statistics
|
||||
|
|
@ -775,14 +778,95 @@ PERCENTILE_RANK_SENSORS = (
|
|||
entity_registry_enabled_default=False, # Available once tomorrow's data arrives
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="price_rank_today_tomorrow",
|
||||
translation_key="price_rank_today_tomorrow",
|
||||
key="current_interval_price_rank_today_tomorrow",
|
||||
translation_key="current_interval_price_rank_today_tomorrow",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None, # Position metric: no statistics
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False, # Advanced overview use case
|
||||
),
|
||||
# ----------------------------------------------------------------
|
||||
# Next interval rank sensors
|
||||
# ----------------------------------------------------------------
|
||||
SensorEntityDescription(
|
||||
key="next_interval_price_rank_today",
|
||||
translation_key="next_interval_price_rank_today",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_interval_price_rank_today_tomorrow",
|
||||
translation_key="next_interval_price_rank_today_tomorrow",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# ----------------------------------------------------------------
|
||||
# Previous interval rank sensors
|
||||
# ----------------------------------------------------------------
|
||||
SensorEntityDescription(
|
||||
key="previous_interval_price_rank_today",
|
||||
translation_key="previous_interval_price_rank_today",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="previous_interval_price_rank_today_tomorrow",
|
||||
translation_key="previous_interval_price_rank_today_tomorrow",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# ----------------------------------------------------------------
|
||||
# Rolling-hour rank sensors (rank of 1h rolling average)
|
||||
# ----------------------------------------------------------------
|
||||
SensorEntityDescription(
|
||||
key="current_hour_price_rank_today",
|
||||
translation_key="current_hour_price_rank_today",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="current_hour_price_rank_today_tomorrow",
|
||||
translation_key="current_hour_price_rank_today_tomorrow",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_hour_price_rank_today",
|
||||
translation_key="next_hour_price_rank_today",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="next_hour_price_rank_today_tomorrow",
|
||||
translation_key="next_hour_price_rank_today_tomorrow",
|
||||
icon="mdi:percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=None,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -250,10 +250,42 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
|
|||
volatility_type="today_tomorrow"
|
||||
),
|
||||
# Price rank sensors (via VolatilityCalculator - reuses same price extraction)
|
||||
"price_rank_today": lambda: volatility_calculator.get_percentile_rank_value(percentile_type="today"),
|
||||
"price_rank_tomorrow": lambda: volatility_calculator.get_percentile_rank_value(percentile_type="tomorrow"),
|
||||
"price_rank_today_tomorrow": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
percentile_type="today_tomorrow"
|
||||
# Current interval rank
|
||||
"current_interval_price_rank_today": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="current_interval", percentile_type="today"
|
||||
),
|
||||
"current_interval_price_rank_tomorrow": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="current_interval", percentile_type="tomorrow"
|
||||
),
|
||||
"current_interval_price_rank_today_tomorrow": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="current_interval", percentile_type="today_tomorrow"
|
||||
),
|
||||
# Next interval rank
|
||||
"next_interval_price_rank_today": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="next_interval", percentile_type="today"
|
||||
),
|
||||
"next_interval_price_rank_today_tomorrow": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="next_interval", percentile_type="today_tomorrow"
|
||||
),
|
||||
# Previous interval rank
|
||||
"previous_interval_price_rank_today": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="previous_interval", percentile_type="today"
|
||||
),
|
||||
"previous_interval_price_rank_today_tomorrow": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="previous_interval", percentile_type="today_tomorrow"
|
||||
),
|
||||
# Rolling-hour rank (1h average)
|
||||
"current_hour_price_rank_today": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="current_hour", percentile_type="today"
|
||||
),
|
||||
"current_hour_price_rank_today_tomorrow": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="current_hour", percentile_type="today_tomorrow"
|
||||
),
|
||||
"next_hour_price_rank_today": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="next_hour", percentile_type="today"
|
||||
),
|
||||
"next_hour_price_rank_today_tomorrow": lambda: volatility_calculator.get_percentile_rank_value(
|
||||
subject="next_hour", percentile_type="today_tomorrow"
|
||||
),
|
||||
# ================================================================
|
||||
# BEST/PEAK PRICE TIMING SENSORS - via TimingCalculator
|
||||
|
|
|
|||
|
|
@ -866,15 +866,6 @@
|
|||
"very_high": "Sehr hoch"
|
||||
}
|
||||
},
|
||||
"price_rank_today": {
|
||||
"name": "Preisrang heute"
|
||||
},
|
||||
"price_rank_tomorrow": {
|
||||
"name": "Preisrang morgen"
|
||||
},
|
||||
"price_rank_today_tomorrow": {
|
||||
"name": "Preisrang heute+morgen"
|
||||
},
|
||||
"best_price_end_time": {
|
||||
"name": "Bestpreis endet"
|
||||
},
|
||||
|
|
@ -1036,6 +1027,39 @@
|
|||
"ready": "Bereit",
|
||||
"error": "Fehler"
|
||||
}
|
||||
},
|
||||
"current_interval_price_rank_today": {
|
||||
"name": "Aktueller Preisrang (heute)"
|
||||
},
|
||||
"current_interval_price_rank_tomorrow": {
|
||||
"name": "Aktueller Preisrang (morgen)"
|
||||
},
|
||||
"current_interval_price_rank_today_tomorrow": {
|
||||
"name": "Aktueller Preisrang (heute+morgen)"
|
||||
},
|
||||
"next_interval_price_rank_today": {
|
||||
"name": "Nächster Preisrang (heute)"
|
||||
},
|
||||
"next_interval_price_rank_today_tomorrow": {
|
||||
"name": "Nächster Preisrang (heute+morgen)"
|
||||
},
|
||||
"previous_interval_price_rank_today": {
|
||||
"name": "Letzter Preisrang (heute)"
|
||||
},
|
||||
"previous_interval_price_rank_today_tomorrow": {
|
||||
"name": "Letzter Preisrang (heute+morgen)"
|
||||
},
|
||||
"current_hour_price_rank_today": {
|
||||
"name": "⌀ Stündlicher Preisrang Aktuell (heute)"
|
||||
},
|
||||
"current_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Stündlicher Preisrang Aktuell (heute+morgen)"
|
||||
},
|
||||
"next_hour_price_rank_today": {
|
||||
"name": "⌀ Stündlicher Preisrang Nächste (heute)"
|
||||
},
|
||||
"next_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Stündlicher Preisrang Nächste (heute+morgen)"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -866,15 +866,6 @@
|
|||
"very_high": "Very High"
|
||||
}
|
||||
},
|
||||
"price_rank_today": {
|
||||
"name": "Today's Price Rank"
|
||||
},
|
||||
"price_rank_tomorrow": {
|
||||
"name": "Tomorrow's Price Rank"
|
||||
},
|
||||
"price_rank_today_tomorrow": {
|
||||
"name": "Today+Tomorrow Price Rank"
|
||||
},
|
||||
"best_price_end_time": {
|
||||
"name": "Best Price End"
|
||||
},
|
||||
|
|
@ -1036,6 +1027,39 @@
|
|||
"ready": "Ready",
|
||||
"error": "Error"
|
||||
}
|
||||
},
|
||||
"current_interval_price_rank_today": {
|
||||
"name": "Current Price Rank (Today)"
|
||||
},
|
||||
"current_interval_price_rank_tomorrow": {
|
||||
"name": "Current Price Rank (Tomorrow)"
|
||||
},
|
||||
"current_interval_price_rank_today_tomorrow": {
|
||||
"name": "Current Price Rank (Today+Tomorrow)"
|
||||
},
|
||||
"next_interval_price_rank_today": {
|
||||
"name": "Next Price Rank (Today)"
|
||||
},
|
||||
"next_interval_price_rank_today_tomorrow": {
|
||||
"name": "Next Price Rank (Today+Tomorrow)"
|
||||
},
|
||||
"previous_interval_price_rank_today": {
|
||||
"name": "Last Price Rank (Today)"
|
||||
},
|
||||
"previous_interval_price_rank_today_tomorrow": {
|
||||
"name": "Last Price Rank (Today+Tomorrow)"
|
||||
},
|
||||
"current_hour_price_rank_today": {
|
||||
"name": "⌀ Hourly Price Current Rank (Today)"
|
||||
},
|
||||
"current_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Hourly Price Current Rank (Today+Tomorrow)"
|
||||
},
|
||||
"next_hour_price_rank_today": {
|
||||
"name": "⌀ Hourly Price Next Rank (Today)"
|
||||
},
|
||||
"next_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Hourly Price Next Rank (Today+Tomorrow)"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -866,15 +866,6 @@
|
|||
"very_high": "Svært Høy"
|
||||
}
|
||||
},
|
||||
"price_rank_today": {
|
||||
"name": "Prisrang i dag"
|
||||
},
|
||||
"price_rank_tomorrow": {
|
||||
"name": "Prisrang i morgen"
|
||||
},
|
||||
"price_rank_today_tomorrow": {
|
||||
"name": "Prisrang i dag+i morgen"
|
||||
},
|
||||
"best_price_end_time": {
|
||||
"name": "Beste pris slutter"
|
||||
},
|
||||
|
|
@ -1036,6 +1027,39 @@
|
|||
"ready": "Klar",
|
||||
"error": "Feil"
|
||||
}
|
||||
},
|
||||
"current_interval_price_rank_today": {
|
||||
"name": "Aktuell prisrang (i dag)"
|
||||
},
|
||||
"current_interval_price_rank_tomorrow": {
|
||||
"name": "Aktuell prisrang (i morgen)"
|
||||
},
|
||||
"current_interval_price_rank_today_tomorrow": {
|
||||
"name": "Aktuell prisrang (i dag+i morgen)"
|
||||
},
|
||||
"next_interval_price_rank_today": {
|
||||
"name": "Neste prisrang (i dag)"
|
||||
},
|
||||
"next_interval_price_rank_today_tomorrow": {
|
||||
"name": "Neste prisrang (i dag+i morgen)"
|
||||
},
|
||||
"previous_interval_price_rank_today": {
|
||||
"name": "Forrige prisrang (i dag)"
|
||||
},
|
||||
"previous_interval_price_rank_today_tomorrow": {
|
||||
"name": "Forrige prisrang (i dag+i morgen)"
|
||||
},
|
||||
"current_hour_price_rank_today": {
|
||||
"name": "⌀ Timesprisrang nå (i dag)"
|
||||
},
|
||||
"current_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Timesprisrang nå (i dag+i morgen)"
|
||||
},
|
||||
"next_hour_price_rank_today": {
|
||||
"name": "⌀ Timesprisrang neste (i dag)"
|
||||
},
|
||||
"next_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Timesprisrang neste (i dag+i morgen)"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -866,15 +866,6 @@
|
|||
"very_high": "Zeer Hoog"
|
||||
}
|
||||
},
|
||||
"price_rank_today": {
|
||||
"name": "Prijsrang vandaag"
|
||||
},
|
||||
"price_rank_tomorrow": {
|
||||
"name": "Prijsrang morgen"
|
||||
},
|
||||
"price_rank_today_tomorrow": {
|
||||
"name": "Prijsrang vandaag+morgen"
|
||||
},
|
||||
"best_price_end_time": {
|
||||
"name": "Beste Prijs Einde"
|
||||
},
|
||||
|
|
@ -1036,6 +1027,39 @@
|
|||
"ready": "Gereed",
|
||||
"error": "Fout"
|
||||
}
|
||||
},
|
||||
"current_interval_price_rank_today": {
|
||||
"name": "Huidige prijsrang (vandaag)"
|
||||
},
|
||||
"current_interval_price_rank_tomorrow": {
|
||||
"name": "Huidige prijsrang (morgen)"
|
||||
},
|
||||
"current_interval_price_rank_today_tomorrow": {
|
||||
"name": "Huidige prijsrang (vandaag+morgen)"
|
||||
},
|
||||
"next_interval_price_rank_today": {
|
||||
"name": "Volgende prijsrang (vandaag)"
|
||||
},
|
||||
"next_interval_price_rank_today_tomorrow": {
|
||||
"name": "Volgende prijsrang (vandaag+morgen)"
|
||||
},
|
||||
"previous_interval_price_rank_today": {
|
||||
"name": "Vorige prijsrang (vandaag)"
|
||||
},
|
||||
"previous_interval_price_rank_today_tomorrow": {
|
||||
"name": "Vorige prijsrang (vandaag+morgen)"
|
||||
},
|
||||
"current_hour_price_rank_today": {
|
||||
"name": "⌀ Uurlijkse prijsrang huidig (vandaag)"
|
||||
},
|
||||
"current_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Uurlijkse prijsrang huidig (vandaag+morgen)"
|
||||
},
|
||||
"next_hour_price_rank_today": {
|
||||
"name": "⌀ Uurlijkse prijsrang volgende (vandaag)"
|
||||
},
|
||||
"next_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Uurlijkse prijsrang volgende (vandaag+morgen)"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -866,15 +866,6 @@
|
|||
"very_high": "Mycket hög"
|
||||
}
|
||||
},
|
||||
"price_rank_today": {
|
||||
"name": "Prisrang idag"
|
||||
},
|
||||
"price_rank_tomorrow": {
|
||||
"name": "Prisrang imorgon"
|
||||
},
|
||||
"price_rank_today_tomorrow": {
|
||||
"name": "Prisrang idag+imorgon"
|
||||
},
|
||||
"best_price_end_time": {
|
||||
"name": "Bästa pris slutar"
|
||||
},
|
||||
|
|
@ -1036,6 +1027,39 @@
|
|||
"ready": "Redo",
|
||||
"error": "Fel"
|
||||
}
|
||||
},
|
||||
"current_interval_price_rank_today": {
|
||||
"name": "Aktuellt prisrang (idag)"
|
||||
},
|
||||
"current_interval_price_rank_tomorrow": {
|
||||
"name": "Aktuellt prisrang (imorgon)"
|
||||
},
|
||||
"current_interval_price_rank_today_tomorrow": {
|
||||
"name": "Aktuellt prisrang (idag+imorgon)"
|
||||
},
|
||||
"next_interval_price_rank_today": {
|
||||
"name": "Nästa prisrang (idag)"
|
||||
},
|
||||
"next_interval_price_rank_today_tomorrow": {
|
||||
"name": "Nästa prisrang (idag+imorgon)"
|
||||
},
|
||||
"previous_interval_price_rank_today": {
|
||||
"name": "Förra prisrang (idag)"
|
||||
},
|
||||
"previous_interval_price_rank_today_tomorrow": {
|
||||
"name": "Förra prisrang (idag+imorgon)"
|
||||
},
|
||||
"current_hour_price_rank_today": {
|
||||
"name": "⌀ Timprisrang aktuell (idag)"
|
||||
},
|
||||
"current_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Timprisrang aktuell (idag+imorgon)"
|
||||
},
|
||||
"next_hour_price_rank_today": {
|
||||
"name": "⌀ Timprisrang nästa (idag)"
|
||||
},
|
||||
"next_hour_price_rank_today_tomorrow": {
|
||||
"name": "⌀ Timprisrang nästa (idag+imorgon)"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
|
|
|
|||
|
|
@ -214,12 +214,20 @@ explanations of each sensor's purpose, attributes, and automation examples.
|
|||
|
||||
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|
||||
|---|---|---|---|---|---|---|
|
||||
| <span id="ref-current_hour_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_hour_price_rank_today` | ⌀ Hourly Price Current Rank (Today) | ⌀ Stündlicher Preisrang Aktuell (heute) | ⌀ Timesprisrang nå (i dag) | ⌀ Uurlijkse prijsrang huidig (vandaag) | ⌀ Timprisrang aktuell (idag) | ❌ |
|
||||
| <span id="ref-current_hour_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_hour_price_rank_today_tomorrow` | ⌀ Hourly Price Current Rank (Today+Tomorrow) | ⌀ Stündlicher Preisrang Aktuell (heute+morgen) | ⌀ Timesprisrang nå (i dag+i morgen) | ⌀ Uurlijkse prijsrang huidig (vandaag+morgen) | ⌀ Timprisrang aktuell (idag+imorgon) | ❌ |
|
||||
| <span id="ref-current_interval_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_interval_price_rank_today` | Current Price Rank (Today) | Aktueller Preisrang (heute) | Aktuell prisrang (i dag) | Huidige prijsrang (vandaag) | Aktuellt prisrang (idag) | ✅ |
|
||||
| <span id="ref-current_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_interval_price_rank_today_tomorrow` | Current Price Rank (Today+Tomorrow) | Aktueller Preisrang (heute+morgen) | Aktuell prisrang (i dag+i morgen) | Huidige prijsrang (vandaag+morgen) | Aktuellt prisrang (idag+imorgon) | ❌ |
|
||||
| <span id="ref-current_interval_price_rank_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_interval_price_rank_tomorrow` | Current Price Rank (Tomorrow) | Aktueller Preisrang (morgen) | Aktuell prisrang (i morgen) | Huidige prijsrang (morgen) | Aktuellt prisrang (imorgon) | ❌ |
|
||||
| <span id="ref-day_pattern_today" class="entity-anchor"></span>`day_pattern_today` | Today's Price Pattern | Preismuster Heute | Prismønster i dag | Prijspatroon Vandaag | Prismönster Idag | ✅ |
|
||||
| <span id="ref-day_pattern_tomorrow" class="entity-anchor"></span>`day_pattern_tomorrow` | Tomorrow's Price Pattern | Preismuster Morgen | Prismønster i morgen | Prijspatroon Morgen | Prismönster Imorgon | ❌ |
|
||||
| <span id="ref-day_pattern_yesterday" class="entity-anchor"></span>`day_pattern_yesterday` | Yesterday's Price Pattern | Preismuster Gestern | Prismønster i går | Prijspatroon Gisteren | Prismönster Igår | ❌ |
|
||||
| <span id="ref-price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`price_rank_today` | Today's Price Rank | Preisrang heute | Prisrang i dag | Prijsrang vandaag | Prisrang idag | ✅ |
|
||||
| <span id="ref-price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`price_rank_today_tomorrow` | Today+Tomorrow Price Rank | Preisrang heute+morgen | Prisrang i dag+i morgen | Prijsrang vandaag+morgen | Prisrang idag+imorgon | ❌ |
|
||||
| <span id="ref-price_rank_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`price_rank_tomorrow` | Tomorrow's Price Rank | Preisrang morgen | Prisrang i morgen | Prijsrang morgen | Prisrang imorgon | ❌ |
|
||||
| <span id="ref-next_hour_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_hour_price_rank_today` | ⌀ Hourly Price Next Rank (Today) | ⌀ Stündlicher Preisrang Nächste (heute) | ⌀ Timesprisrang neste (i dag) | ⌀ Uurlijkse prijsrang volgende (vandaag) | ⌀ Timprisrang nästa (idag) | ❌ |
|
||||
| <span id="ref-next_hour_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_hour_price_rank_today_tomorrow` | ⌀ Hourly Price Next Rank (Today+Tomorrow) | ⌀ Stündlicher Preisrang Nächste (heute+morgen) | ⌀ Timesprisrang neste (i dag+i morgen) | ⌀ Uurlijkse prijsrang volgende (vandaag+morgen) | ⌀ Timprisrang nästa (idag+imorgon) | ❌ |
|
||||
| <span id="ref-next_interval_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_interval_price_rank_today` | Next Price Rank (Today) | Nächster Preisrang (heute) | Neste prisrang (i dag) | Volgende prijsrang (vandaag) | Nästa prisrang (idag) | ❌ |
|
||||
| <span id="ref-next_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_interval_price_rank_today_tomorrow` | Next Price Rank (Today+Tomorrow) | Nächster Preisrang (heute+morgen) | Neste prisrang (i dag+i morgen) | Volgende prijsrang (vandaag+morgen) | Nästa prisrang (idag+imorgon) | ❌ |
|
||||
| <span id="ref-previous_interval_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`previous_interval_price_rank_today` | Last Price Rank (Today) | Letzter Preisrang (heute) | Forrige prisrang (i dag) | Vorige prijsrang (vandaag) | Förra prisrang (idag) | ❌ |
|
||||
| <span id="ref-previous_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`previous_interval_price_rank_today_tomorrow` | Last Price Rank (Today+Tomorrow) | Letzter Preisrang (heute+morgen) | Forrige prisrang (i dag+i morgen) | Vorige prijsrang (vandaag+morgen) | Förra prisrang (idag+imorgon) | ❌ |
|
||||
## Binary Sensors
|
||||
|
||||
### Binary Sensors
|
||||
|
|
|
|||
|
|
@ -139,44 +139,81 @@ The `price_spike_count` attribute (Tukey fence method: Q25 − 1.5×IQR to Q75 +
|
|||
|
||||
## Price Rank Sensors (Percentile Rank)
|
||||
|
||||
The price rank sensors answer the simple question: **"Is the current price cheap or expensive compared to the rest of the day?"**
|
||||
The price rank sensors answer the simple question: **"Is this price cheap or expensive compared to the rest of the day?"**
|
||||
|
||||
Unlike the volatility sensors (which measure the _shape_ of the entire price distribution), price rank sensors place the _current price_ within that distribution — technically its **percentile rank**. A value of **0% means cheapest interval of the day**, while a value near **99% means most expensive**.
|
||||
Unlike the volatility sensors (which measure the _shape_ of the entire price distribution), price rank sensors place a _specific price_ within that distribution — technically its **percentile rank**. A value of **0% means cheapest interval of the reference set**, while a value near **99% means most expensive**.
|
||||
|
||||
Each sensor ranks a different **subject price** against a **reference window**:
|
||||
|
||||
- **Subject** — Which price is being ranked: current interval, next interval, previous interval, or the rolling hourly average
|
||||
- **Reference window** — Which pool of slots to compare against: today only, tomorrow only, or today+tomorrow combined
|
||||
|
||||
### How It Works (Percentile Rank Formula)
|
||||
|
||||
```
|
||||
Price rank (percentile rank) = (number of intervals strictly cheaper than now) ÷ total intervals × 100
|
||||
Price rank (percentile rank) = (number of intervals strictly cheaper than subject) ÷ total intervals × 100
|
||||
```
|
||||
|
||||
The cheapest interval always returns 0% — you can use `state == 0` to detect the absolute cheapest moment.
|
||||
|
||||
### Available Sensors
|
||||
|
||||
| Sensor | Reference Set | Enabled by Default |
|
||||
| ------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------ |
|
||||
| <EntityRef id="price_rank_today">Today's Price Rank</EntityRef> | All of today's 96 quarter-hour intervals | ✅ Yes |
|
||||
| <EntityRef id="price_rank_tomorrow">Tomorrow's Price Rank</EntityRef> | All of tomorrow's 96 intervals (once available) | ❌ No |
|
||||
| <EntityRef id="price_rank_today_tomorrow">Today+Tomorrow Price Rank</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
|
||||
**Current interval** (price of the active quarter-hour):
|
||||
|
||||
| Sensor | Reference Set | Enabled by Default |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------ |
|
||||
| <EntityRef id="current_interval_price_rank_today">Current Price Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ✅ Yes |
|
||||
| <EntityRef id="current_interval_price_rank_tomorrow">Current Price Rank (Tomorrow)</EntityRef> | Tomorrow's 96 intervals (once avail.) | ❌ No |
|
||||
| <EntityRef id="current_interval_price_rank_today_tomorrow">Current Price Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
|
||||
|
||||
**Next interval** (price of the upcoming quarter-hour):
|
||||
|
||||
| Sensor | Reference Set | Enabled by Default |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------ |
|
||||
| <EntityRef id="next_interval_price_rank_today">Next Price Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ❌ No |
|
||||
| <EntityRef id="next_interval_price_rank_today_tomorrow">Next Price Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
|
||||
|
||||
**Previous interval** (price of the just-ended quarter-hour):
|
||||
|
||||
| Sensor | Reference Set | Enabled by Default |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------ |
|
||||
| <EntityRef id="previous_interval_price_rank_today">Last Price Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ❌ No |
|
||||
| <EntityRef id="previous_interval_price_rank_today_tomorrow">Last Price Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
|
||||
|
||||
**Rolling hourly average** (5-interval window, ~1 hour):
|
||||
|
||||
| Sensor | Reference Set | Enabled by Default |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------ |
|
||||
| <EntityRef id="current_hour_price_rank_today">⌀ Hourly Price Current Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ❌ No |
|
||||
| <EntityRef id="current_hour_price_rank_today_tomorrow">⌀ Hourly Price Current Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
|
||||
| <EntityRef id="next_hour_price_rank_today">⌀ Hourly Price Next Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ❌ No |
|
||||
| <EntityRef id="next_hour_price_rank_today_tomorrow">⌀ Hourly Price Next Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
|
||||
|
||||
### Key Attributes
|
||||
|
||||
All price rank sensors share these attributes:
|
||||
All price rank sensors share most of these attributes. The price attribute key reflects the subject:
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| -------------------- | --------------------------------------------- | ------- |
|
||||
| `current_price` | The price being ranked | `14.2` |
|
||||
| `prices_below_count` | How many intervals are strictly cheaper | `23` |
|
||||
| `interval_count` | Total intervals in the reference set | `96` |
|
||||
| `reference_min` | The cheapest price in the reference set | `8.1` |
|
||||
| `reference_max` | The most expensive price in the reference set | `27.3` |
|
||||
| `reference_mean` | Average price of the reference set | `15.8` |
|
||||
| Attribute | Description | Subject |
|
||||
| ------------------------ | -------------------------------------------------------- | ------------------ |
|
||||
| `current_price` | The price being ranked (current interval) | Current interval |
|
||||
| `next_price` | The price being ranked (next interval) | Next interval |
|
||||
| `previous_price` | The price being ranked (previous interval) | Previous interval |
|
||||
| `current_hour_avg_price` | The rolling average being ranked (current hour) | Current hour avg |
|
||||
| `next_hour_avg_price` | The rolling average being ranked (next hour) | Next hour avg |
|
||||
| `prices_below_count` | How many reference intervals are strictly cheaper | All sensors |
|
||||
| `interval_count` | Total intervals in the reference set | All sensors |
|
||||
| `reference_min` | The cheapest price in the reference set | All sensors |
|
||||
| `reference_max` | The most expensive price in the reference set | All sensors |
|
||||
| `reference_mean` | Average price of the reference set | All sensors |
|
||||
|
||||
### When to Use Which Sensor
|
||||
|
||||
- **`price_rank_today`** — For same-day scheduling. "Is now within the cheapest quarter of today? (< 25%)"
|
||||
- **`price_rank_tomorrow`** — To compare today's price against what tomorrow offers. "Is it worth waiting until tomorrow?"
|
||||
- **`price_rank_today_tomorrow`** — Broadest view for flexible tasks. "Is this among the cheapest moments of a 48-hour window?"
|
||||
- **Current (Today)** — Same-day scheduling. "Is the active quarter-hour within the cheapest 25% of today?"
|
||||
- **Next (Today)** — Prepare for the next interval. "Should I pre-heat now so the device runs in the coming cheap slot?"
|
||||
- **Current (Today+Tomorrow)** — Broadest view for flexible tasks. "Is this among the cheapest moments of a 48-hour window?"
|
||||
- **Current (Tomorrow)** — Decide whether to wait until tomorrow. "Is today's price worse than what tomorrow offers?"
|
||||
- **⌀ Hourly Current (Today)** — For tasks that take about an hour. "Is this hour cheap enough to start a 60-minute cycle?"
|
||||
- **⌀ Hourly Next (Today)** — One-hour look-ahead. "Will the upcoming hour be cheap enough to start now?"
|
||||
|
||||
### Usage in Automations
|
||||
|
||||
|
|
@ -188,7 +225,7 @@ automation:
|
|||
- alias: "Start dishwasher at cheapest time of day"
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.<home_name>_today_s_price_rank
|
||||
entity_id: sensor.<home_name>_current_price_rank_today
|
||||
below: 25
|
||||
condition:
|
||||
- condition: state
|
||||
|
|
@ -214,10 +251,32 @@ automation:
|
|||
# Only postpone if tomorrow's cheapest quartile is better than the current price
|
||||
- condition: template
|
||||
value_template: >
|
||||
{{ states('sensor.<home_name>_tomorrow_s_price_rank') | float(100) < 25 }}
|
||||
{{ states('sensor.<home_name>_current_price_rank_tomorrow') | float(100) < 25 }}
|
||||
action:
|
||||
- service: input_boolean.turn_off
|
||||
entity_id: input_boolean.ev_charge_tonight
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Show YAML: Pre-heat when the next interval is cheap</summary>
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: "Pre-heat if next interval is top quartile cheapest"
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
minutes: "/15"
|
||||
condition:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.<home_name>_next_price_rank_today
|
||||
below: 25
|
||||
action:
|
||||
- service: climate.set_hvac_mode
|
||||
entity_id: climate.living_room
|
||||
data:
|
||||
hvac_mode: heat
|
||||
```
|
||||
|
||||
</details>
|
||||
|
|
|
|||
Loading…
Reference in a new issue