mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
Complete terminology migration from confusing "major/minor" to clearer
"base/subunit" currency naming throughout entire codebase, translations,
documentation, tests, and services.
BREAKING CHANGES:
1. **Service API Parameters Renamed**:
- `get_chartdata`: `minor_currency` → `subunit_currency`
- `get_apexcharts_yaml`: Updated service_data references from
`minor_currency: true` to `subunit_currency: true`
- All automations/scripts using these parameters MUST be updated
2. **Configuration Option Key Changed**:
- Config entry option: Display mode setting now uses new terminology
- Internal key: `currency_display_mode` values remain "base"/"subunit"
- User-facing labels updated in all 5 languages (de, en, nb, nl, sv)
3. **Sensor Entity Key Renamed**:
- `current_interval_price_major` → `current_interval_price_base`
- Entity ID changes: `sensor.tibber_home_current_interval_price_major`
→ `sensor.tibber_home_current_interval_price_base`
- Energy Dashboard configurations MUST update entity references
4. **Function Signatures Changed**:
- `format_price_unit_major()` → `format_price_unit_base()`
- `format_price_unit_minor()` → `format_price_unit_subunit()`
- `get_price_value()`: Parameter `in_euro` deprecated in favor of
`config_entry` (backward compatible for now)
5. **Translation Keys Renamed**:
- All language files: Sensor translation key
`current_interval_price_major` → `current_interval_price_base`
- Service parameter descriptions updated in all languages
- Selector options updated: Display mode dropdown values
Changes by Category:
**Core Code (Python)**:
- const.py: Renamed all format_price_unit_*() functions, updated docstrings
- entity_utils/helpers.py: Updated get_price_value() with config-driven
conversion and backward-compatible in_euro parameter
- sensor/__init__.py: Added display mode filtering for base currency sensor
- sensor/core.py:
* Implemented suggested_display_precision property for dynamic decimal places
* Updated native_unit_of_measurement to use get_display_unit_string()
* Updated all price conversion calls to use config_entry parameter
- sensor/definitions.py: Renamed entity key and updated all
suggested_display_precision values (2 decimals for most sensors)
- sensor/calculators/*.py: Updated all price conversion calls (8 calculators)
- sensor/helpers.py: Updated aggregate_price_data() signature with config_entry
- sensor/attributes/future.py: Updated future price attributes conversion
**Services**:
- services/chartdata.py: Renamed parameter minor_currency → subunit_currency
throughout (53 occurrences), updated metadata calculation
- services/apexcharts.py: Updated service_data references in generated YAML
- services/formatters.py: Renamed parameter use_minor_currency →
use_subunit_currency in aggregate_hourly_exact() and get_period_data()
- sensor/chart_metadata.py: Updated default parameter name
**Translations (5 Languages)**:
- All /translations/*.json:
* Added new config step "display_settings" with comprehensive explanations
* Renamed current_interval_price_major → current_interval_price_base
* Updated service parameter descriptions (subunit_currency)
* Added selector.currency_display_mode.options with translated labels
- All /custom_translations/*.json:
* Renamed sensor description keys
* Updated chart_metadata usage_tips references
**Documentation**:
- docs/user/docs/actions.md: Updated parameter table and feature list
- docs/user/versioned_docs/version-v0.21.0/actions.md: Backported changes
**Tests**:
- Updated 7 test files with renamed parameters and conversion logic:
* test_connect_segments.py: Renamed minor/major to subunit/base
* test_period_data_format.py: Updated period price conversion tests
* test_avg_none_fallback.py: Fixed tuple unpacking for new return format
* test_best_price_e2e.py: Added config_entry parameter to all calls
* test_cache_validity.py: Fixed cache data structure (price_info key)
* test_coordinator_shutdown.py: Added repair_manager mock
* test_midnight_turnover.py: Added config_entry parameter
* test_peak_price_e2e.py: Added config_entry parameter, fixed price_avg → price_mean
* test_percentage_calculations.py: Added config_entry mock
**Coordinator/Period Calculation**:
- coordinator/periods.py: Added config_entry parameter to
calculate_periods_with_relaxation() calls (2 locations)
Migration Guide:
1. **Update Service Calls in Automations/Scripts**:
\`\`\`yaml
# Before:
service: tibber_prices.get_chartdata
data:
minor_currency: true
# After:
service: tibber_prices.get_chartdata
data:
subunit_currency: true
\`\`\`
2. **Update Energy Dashboard Configuration**:
- Settings → Dashboards → Energy
- Replace sensor entity:
`sensor.tibber_home_current_interval_price_major` →
`sensor.tibber_home_current_interval_price_base`
3. **Review Integration Configuration**:
- Settings → Devices & Services → Tibber Prices → Configure
- New "Currency Display Settings" step added
- Default mode depends on currency (EUR → subunit, Scandinavian → base)
Rationale:
The "major/minor" terminology was confusing and didn't clearly communicate:
- **Major** → Unclear if this means "primary" or "large value"
- **Minor** → Easily confused with "less important" rather than "smaller unit"
New terminology is precise and self-explanatory:
- **Base currency** → Standard ISO currency (€, kr, $, £)
- **Subunit currency** → Fractional unit (ct, øre, ¢, p)
This aligns with:
- International terminology (ISO 4217 standard)
- Banking/financial industry conventions
- User expectations from payment processing systems
Impact: Aligns currency terminology with international standards. Users must
update service calls, automations, and Energy Dashboard configuration after
upgrade.
Refs: User feedback session (December 2025) identified terminology confusion
449 lines
16 KiB
Python
449 lines
16 KiB
Python
"""
|
|
Test sensor-to-timer assignment correctness.
|
|
|
|
This tests the CRITICAL mapping between sensor entities and update timers:
|
|
- TIME_SENSITIVE sensors → Timer #2 (quarter-hour: :00, :15, :30, :45)
|
|
- MINUTE_UPDATE sensors → Timer #3 (minute: :00, :30)
|
|
- All other sensors → No timer (only update on API data arrival)
|
|
|
|
Ensures:
|
|
1. Each sensor is assigned to the correct timer
|
|
2. Timer constants match sensor definitions
|
|
3. No sensors are missing from or incorrectly added to timer groups
|
|
"""
|
|
|
|
from custom_components.tibber_prices.binary_sensor.definitions import (
|
|
ENTITY_DESCRIPTIONS as BINARY_SENSOR_ENTITY_DESCRIPTIONS,
|
|
)
|
|
from custom_components.tibber_prices.coordinator.constants import (
|
|
MINUTE_UPDATE_ENTITY_KEYS,
|
|
TIME_SENSITIVE_ENTITY_KEYS,
|
|
)
|
|
from custom_components.tibber_prices.sensor.definitions import ENTITY_DESCRIPTIONS
|
|
|
|
|
|
def test_time_sensitive_sensors_are_valid() -> None:
|
|
"""
|
|
Test that all TIME_SENSITIVE_ENTITY_KEYS correspond to actual sensors.
|
|
|
|
Timer #2 (quarter-hour) should only trigger for sensors that exist.
|
|
"""
|
|
all_sensor_keys = {desc.key for desc in ENTITY_DESCRIPTIONS}
|
|
all_binary_sensor_keys = {desc.key for desc in BINARY_SENSOR_ENTITY_DESCRIPTIONS}
|
|
all_entity_keys = all_sensor_keys | all_binary_sensor_keys
|
|
|
|
for entity_key in TIME_SENSITIVE_ENTITY_KEYS:
|
|
assert entity_key in all_entity_keys, (
|
|
f"TIME_SENSITIVE key '{entity_key}' not found in sensor/binary_sensor definitions"
|
|
)
|
|
|
|
|
|
def test_minute_update_sensors_are_valid() -> None:
|
|
"""
|
|
Test that all MINUTE_UPDATE_ENTITY_KEYS correspond to actual sensors.
|
|
|
|
Timer #3 (minute) should only trigger for sensors that exist.
|
|
"""
|
|
all_sensor_keys = {desc.key for desc in ENTITY_DESCRIPTIONS}
|
|
all_binary_sensor_keys = {desc.key for desc in BINARY_SENSOR_ENTITY_DESCRIPTIONS}
|
|
all_entity_keys = all_sensor_keys | all_binary_sensor_keys
|
|
|
|
for entity_key in MINUTE_UPDATE_ENTITY_KEYS:
|
|
assert entity_key in all_entity_keys, (
|
|
f"MINUTE_UPDATE key '{entity_key}' not found in sensor/binary_sensor definitions"
|
|
)
|
|
|
|
|
|
def test_no_overlap_between_timer_groups() -> None:
|
|
"""
|
|
Test that TIME_SENSITIVE and MINUTE_UPDATE groups are mutually exclusive.
|
|
|
|
A sensor should never be in both timer groups simultaneously.
|
|
This would cause duplicate updates and wasted resources.
|
|
"""
|
|
overlap = TIME_SENSITIVE_ENTITY_KEYS & MINUTE_UPDATE_ENTITY_KEYS
|
|
|
|
assert not overlap, (
|
|
f"Sensors should not be in both TIME_SENSITIVE and MINUTE_UPDATE: {overlap}\n"
|
|
"Each sensor should use only ONE timer for updates."
|
|
)
|
|
|
|
|
|
def test_interval_sensors_use_quarter_hour_timer() -> None:
|
|
"""
|
|
Test that interval-based sensors (current/next/previous) use Timer #2.
|
|
|
|
These sensors need updates every 15 minutes because they reference
|
|
specific 15-minute intervals that change at quarter-hour boundaries.
|
|
"""
|
|
interval_sensors = [
|
|
"current_interval_price",
|
|
"next_interval_price",
|
|
"previous_interval_price",
|
|
"current_interval_price_level",
|
|
"next_interval_price_level",
|
|
"previous_interval_price_level",
|
|
"current_interval_price_rating",
|
|
"next_interval_price_rating",
|
|
"previous_interval_price_rating",
|
|
]
|
|
|
|
for sensor_key in interval_sensors:
|
|
assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Interval sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
|
|
)
|
|
|
|
|
|
def test_rolling_hour_sensors_use_quarter_hour_timer() -> None:
|
|
"""
|
|
Test that rolling hour sensors (5-interval windows) use Timer #2.
|
|
|
|
Rolling hour calculations depend on current interval position,
|
|
which changes every 15 minutes.
|
|
"""
|
|
rolling_hour_sensors = [
|
|
"current_hour_average_price",
|
|
"next_hour_average_price",
|
|
"current_hour_price_level",
|
|
"next_hour_price_level",
|
|
"current_hour_price_rating",
|
|
"next_hour_price_rating",
|
|
]
|
|
|
|
for sensor_key in rolling_hour_sensors:
|
|
assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Rolling hour sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
|
|
)
|
|
|
|
|
|
def test_future_avg_sensors_use_quarter_hour_timer() -> None:
|
|
"""
|
|
Test that future N-hour average sensors use Timer #2.
|
|
|
|
Future averages calculate rolling windows starting from "next interval",
|
|
which changes every 15 minutes.
|
|
"""
|
|
future_avg_sensors = [
|
|
"next_avg_1h",
|
|
"next_avg_2h",
|
|
"next_avg_3h",
|
|
"next_avg_4h",
|
|
"next_avg_5h",
|
|
"next_avg_6h",
|
|
"next_avg_8h",
|
|
"next_avg_12h",
|
|
]
|
|
|
|
for sensor_key in future_avg_sensors:
|
|
assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Future avg sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
|
|
)
|
|
|
|
|
|
def test_trend_sensors_use_quarter_hour_timer() -> None:
|
|
"""
|
|
Test that price trend sensors use Timer #2.
|
|
|
|
Trend analysis depends on current interval position and
|
|
needs updates at quarter-hour boundaries.
|
|
"""
|
|
trend_sensors = [
|
|
"current_price_trend",
|
|
"next_price_trend_change",
|
|
"price_trend_1h",
|
|
"price_trend_2h",
|
|
"price_trend_3h",
|
|
"price_trend_4h",
|
|
"price_trend_5h",
|
|
"price_trend_6h",
|
|
"price_trend_8h",
|
|
"price_trend_12h",
|
|
]
|
|
|
|
for sensor_key in trend_sensors:
|
|
assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Trend sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
|
|
)
|
|
|
|
|
|
def test_window_24h_sensors_use_quarter_hour_timer() -> None:
|
|
"""
|
|
Test that trailing/leading 24h window sensors use Timer #2.
|
|
|
|
24h windows are calculated relative to current interval,
|
|
which changes every 15 minutes.
|
|
"""
|
|
window_24h_sensors = [
|
|
"trailing_price_average",
|
|
"leading_price_average",
|
|
"trailing_price_min",
|
|
"trailing_price_max",
|
|
"leading_price_min",
|
|
"leading_price_max",
|
|
]
|
|
|
|
for sensor_key in window_24h_sensors:
|
|
assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"24h window sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
|
|
)
|
|
|
|
|
|
def test_period_binary_sensors_use_quarter_hour_timer() -> None:
|
|
"""
|
|
Test that best/peak price period binary sensors use Timer #2.
|
|
|
|
Binary sensors check if current time is within a period.
|
|
Periods can only change at quarter-hour interval boundaries.
|
|
"""
|
|
period_binary_sensors = [
|
|
"best_price_period",
|
|
"peak_price_period",
|
|
]
|
|
|
|
for sensor_key in period_binary_sensors:
|
|
assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Period binary sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
|
|
)
|
|
|
|
|
|
def test_period_timestamp_sensors_use_quarter_hour_timer() -> None:
|
|
"""
|
|
Test that period timestamp sensors (end_time, next_start_time) use Timer #2.
|
|
|
|
Timestamp sensors report when periods end/start. Since periods can only
|
|
change at quarter-hour boundaries (intervals), they only need quarter-hour updates.
|
|
"""
|
|
timestamp_sensors = [
|
|
"best_price_end_time",
|
|
"best_price_next_start_time",
|
|
"peak_price_end_time",
|
|
"peak_price_next_start_time",
|
|
]
|
|
|
|
for sensor_key in timestamp_sensors:
|
|
assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Timestamp sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
|
|
)
|
|
|
|
|
|
def test_timing_sensors_use_minute_timer() -> None:
|
|
"""
|
|
Test that countdown/progress timing sensors use Timer #3.
|
|
|
|
These sensors track time remaining and progress percentage within periods.
|
|
They need minute-by-minute updates for accurate countdown displays.
|
|
|
|
IMPORTANT: Timestamp sensors (end_time, next_start_time) do NOT use Timer #3
|
|
because periods can only change at quarter-hour boundaries.
|
|
"""
|
|
timing_sensors = [
|
|
"best_price_remaining_minutes",
|
|
"best_price_progress",
|
|
"best_price_next_in_minutes", # Corrected from best_price_next_start_minutes
|
|
"peak_price_remaining_minutes",
|
|
"peak_price_progress",
|
|
"peak_price_next_in_minutes", # Corrected from peak_price_next_start_minutes
|
|
]
|
|
|
|
for sensor_key in timing_sensors:
|
|
assert sensor_key in MINUTE_UPDATE_ENTITY_KEYS, (
|
|
f"Timing sensor '{sensor_key}' should be MINUTE_UPDATE (Timer #3)"
|
|
)
|
|
|
|
# Also verify it's NOT in TIME_SENSITIVE (no double updates)
|
|
assert sensor_key not in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Timing sensor '{sensor_key}' should NOT be in TIME_SENSITIVE\n"
|
|
"Minute updates are sufficient for countdown/progress tracking."
|
|
)
|
|
|
|
|
|
def test_lifecycle_sensor_uses_quarter_hour_timer() -> None:
|
|
"""
|
|
Test that data lifecycle status sensor uses Timer #2.
|
|
|
|
The lifecycle sensor needs quarter-hour updates to detect:
|
|
- Turnover pending at 23:45 (quarter-hour boundary)
|
|
- Turnover completed after midnight API update
|
|
"""
|
|
assert "data_lifecycle_status" in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
"Lifecycle sensor needs quarter-hour updates to detect turnover_pending\n"
|
|
"at 23:45 (last interval before midnight)"
|
|
)
|
|
|
|
|
|
def test_daily_stat_sensors_not_in_timers() -> None:
|
|
"""
|
|
Test that daily statistic sensors (min/max/avg) do NOT use timers.
|
|
|
|
Daily stats don't depend on current time - they represent full-day aggregates.
|
|
They only need updates when new API data arrives (not time-dependent).
|
|
"""
|
|
daily_stat_sensors = [
|
|
# Today/tomorrow min prices
|
|
"daily_min_price_today",
|
|
"daily_min_price_tomorrow",
|
|
# Today/tomorrow max prices
|
|
"daily_max_price_today",
|
|
"daily_max_price_tomorrow",
|
|
# Today/tomorrow averages
|
|
"daily_average_price_today",
|
|
"daily_average_price_tomorrow",
|
|
# Daily price levels
|
|
"daily_price_level_today",
|
|
"daily_price_level_tomorrow",
|
|
# Daily price ratings
|
|
"daily_price_rating_today",
|
|
"daily_price_rating_tomorrow",
|
|
]
|
|
|
|
for sensor_key in daily_stat_sensors:
|
|
assert sensor_key not in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Daily stat sensor '{sensor_key}' should NOT use Timer #2\n"
|
|
"Daily statistics don't depend on current time - only on API data arrival."
|
|
)
|
|
assert sensor_key not in MINUTE_UPDATE_ENTITY_KEYS, (
|
|
f"Daily stat sensor '{sensor_key}' should NOT use Timer #3\n"
|
|
"Daily statistics don't need minute-by-minute updates."
|
|
)
|
|
|
|
|
|
def test_volatility_sensors_not_in_timers() -> None:
|
|
"""
|
|
Test that volatility sensors do NOT use timers.
|
|
|
|
Volatility analyzes price variation over fixed time windows.
|
|
Values only change when new API data arrives (not time-dependent).
|
|
"""
|
|
volatility_sensors = [
|
|
"today_volatility_level",
|
|
"tomorrow_volatility_level",
|
|
"yesterday_volatility_level",
|
|
"next_24h_volatility_level",
|
|
]
|
|
|
|
for sensor_key in volatility_sensors:
|
|
assert sensor_key not in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Volatility sensor '{sensor_key}' should NOT use Timer #2\n"
|
|
"Volatility calculates over fixed time windows - not time-dependent."
|
|
)
|
|
assert sensor_key not in MINUTE_UPDATE_ENTITY_KEYS, (
|
|
f"Volatility sensor '{sensor_key}' should NOT use Timer #3\n"
|
|
"Volatility doesn't need minute-by-minute updates."
|
|
)
|
|
|
|
|
|
def test_diagnostic_sensors_not_in_timers() -> None:
|
|
"""
|
|
Test that diagnostic/metadata sensors do NOT use timers.
|
|
|
|
Diagnostic sensors report static metadata or system state.
|
|
They only update when configuration changes or new API data arrives.
|
|
"""
|
|
diagnostic_sensors = [
|
|
"data_last_updated",
|
|
"home_id",
|
|
"currency_code",
|
|
"price_unit",
|
|
"grid_company",
|
|
"price_level",
|
|
"address_line1",
|
|
"address_line2",
|
|
"address_line3",
|
|
"zip_code",
|
|
"city",
|
|
"country",
|
|
"latitude",
|
|
"longitude",
|
|
"time_zone",
|
|
"estimated_annual_consumption",
|
|
"subscription_status",
|
|
"chart_data_export",
|
|
]
|
|
|
|
for sensor_key in diagnostic_sensors:
|
|
# Skip data_lifecycle_status - it needs quarter-hour updates
|
|
if sensor_key == "data_lifecycle_status":
|
|
continue
|
|
|
|
assert sensor_key not in TIME_SENSITIVE_ENTITY_KEYS, (
|
|
f"Diagnostic sensor '{sensor_key}' should NOT use Timer #2\nDiagnostic data doesn't depend on current time."
|
|
)
|
|
assert sensor_key not in MINUTE_UPDATE_ENTITY_KEYS, (
|
|
f"Diagnostic sensor '{sensor_key}' should NOT use Timer #3\n"
|
|
"Diagnostic data doesn't need minute-by-minute updates."
|
|
)
|
|
|
|
|
|
def test_timer_constants_are_comprehensive() -> None:
|
|
"""
|
|
Test that timer constants account for all time-dependent sensors.
|
|
|
|
Verifies no time-dependent sensors are missing from timer groups.
|
|
This is a safety check to catch sensors that need timers but don't have them.
|
|
"""
|
|
all_sensor_keys = {desc.key for desc in ENTITY_DESCRIPTIONS}
|
|
all_binary_sensor_keys = {desc.key for desc in BINARY_SENSOR_ENTITY_DESCRIPTIONS}
|
|
all_entity_keys = all_sensor_keys | all_binary_sensor_keys
|
|
sensors_with_timers = TIME_SENSITIVE_ENTITY_KEYS | MINUTE_UPDATE_ENTITY_KEYS
|
|
|
|
# Expected time-dependent sensor patterns
|
|
time_dependent_patterns = [
|
|
"current_",
|
|
"next_",
|
|
"previous_",
|
|
"trailing_",
|
|
"leading_",
|
|
"_remaining_",
|
|
"_progress",
|
|
"_next_in_", # Corrected from _next_start_
|
|
"_end_time",
|
|
"_period", # Binary sensors checking if NOW is in period
|
|
"price_trend_",
|
|
"next_avg_",
|
|
]
|
|
|
|
# Known exceptions that look time-dependent but aren't
|
|
known_exceptions = {
|
|
"data_last_updated", # Timestamp of last update, not time-dependent
|
|
"next_24h_volatility", # Uses fixed 24h window from current time, updated on API data
|
|
"current_interval_price_base", # Duplicate of current_interval_price (just different unit)
|
|
"best_price_period_duration", # Duration in minutes, doesn't change minute-by-minute
|
|
"peak_price_period_duration", # Duration in minutes, doesn't change minute-by-minute
|
|
}
|
|
|
|
potentially_missing = [
|
|
sensor_key
|
|
for sensor_key in all_entity_keys
|
|
if (
|
|
any(pattern in sensor_key for pattern in time_dependent_patterns)
|
|
and sensor_key not in sensors_with_timers
|
|
and sensor_key not in known_exceptions
|
|
)
|
|
]
|
|
|
|
assert not potentially_missing, (
|
|
f"These sensors appear time-dependent but aren't in any timer group:\n"
|
|
f"{potentially_missing}\n\n"
|
|
"If they truly need time-based updates, add them to TIME_SENSITIVE_ENTITY_KEYS\n"
|
|
"or MINUTE_UPDATE_ENTITY_KEYS in coordinator/constants.py"
|
|
)
|
|
|
|
|
|
def test_timer_group_sizes() -> None:
|
|
"""
|
|
Test timer group sizes as documentation/regression detection.
|
|
|
|
This isn't a strict requirement, but significant changes in group sizes
|
|
might indicate accidental additions/removals.
|
|
"""
|
|
# As of Nov 2025
|
|
expected_time_sensitive_min = 40 # At least 40 sensors
|
|
expected_minute_update = 6 # Exactly 6 timing sensors
|
|
|
|
assert len(TIME_SENSITIVE_ENTITY_KEYS) >= expected_time_sensitive_min, (
|
|
f"Expected at least {expected_time_sensitive_min} TIME_SENSITIVE sensors, got {len(TIME_SENSITIVE_ENTITY_KEYS)}"
|
|
)
|
|
|
|
assert len(MINUTE_UPDATE_ENTITY_KEYS) == expected_minute_update, (
|
|
f"Expected exactly {expected_minute_update} MINUTE_UPDATE sensors, got {len(MINUTE_UPDATE_ENTITY_KEYS)}"
|
|
)
|