diff --git a/custom_components/tibber_prices/binary_sensor/attributes.py b/custom_components/tibber_prices/binary_sensor/attributes.py index 2e413b9..cd8eb8d 100644 --- a/custom_components/tibber_prices/binary_sensor/attributes.py +++ b/custom_components/tibber_prices/binary_sensor/attributes.py @@ -445,7 +445,7 @@ async def build_async_extra_state_attributes( # noqa: PLR0913 position="end", ) - return attributes if attributes else None + return attributes or None def build_sync_extra_state_attributes( # noqa: PLR0913 @@ -508,4 +508,4 @@ def build_sync_extra_state_attributes( # noqa: PLR0913 position="end", ) - return attributes if attributes else None + return attributes or None diff --git a/custom_components/tibber_prices/coordinator/period_handlers/level_filtering.py b/custom_components/tibber_prices/coordinator/period_handlers/level_filtering.py index 9714942..2fe3e36 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/level_filtering.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/level_filtering.py @@ -133,20 +133,30 @@ def check_interval_criteria( # - Peak price (reverse_sort=True): daily MAXIMUM # - Best price (reverse_sort=False): daily MINIMUM # - # Flex band calculation (using absolute values): - # - Peak price: [max - max*flex, max] → accept prices near the maximum - # - Best price: [min, min + min*flex] → accept prices near the minimum + # Flex base = max(price_span, abs(ref_price)): + # - On V-shape days (tiny minimum, large span): span wins → meaningful flex band + # - On flat days (large minimum, small span): ref_price wins → same as before # - # Examples with flex=20%: - # - Peak: max=30 ct → accept [24, 30] ct (prices ≥ 24 ct) - # - Best: min=10 ct → accept [10, 12] ct (prices ≤ 12 ct) + # WHY NOT plain ref_price * flex: When daily_min is a single low outlier + # (e.g., min=1 ct, avg=19 ct), the flex band collapses to near-zero + # (1 ct * 15% = 0.15 ct) and no period of sufficient length can be found. + # + # WHY NOT plain span * flex: On flat days (e.g., min=30 ct, span=3 ct), + # this makes the band much narrower than before, breaking existing behaviour. + # + # Examples with flex=15%: + # - V-shape: min=1 ct, avg=19 ct → span=18 ct → flex_base=18 → threshold=1+2.7=3.7 ct (spans fixed) + # - Flat: min=30 ct, avg=33 ct → span=3 ct → flex_base=30 → threshold=30+4.5=34.5 ct (unchanged) + # - Normal: min=10 ct, avg=20 ct → span=10 ct → flex_base=10 → threshold=10+1.5=11.5 ct (unchanged) - if criteria.ref_price == 0: - # Zero reference: flex has no effect, use strict equality + price_span = abs(criteria.avg_price - criteria.ref_price) + flex_base = max(price_span, abs(criteria.ref_price)) + + if flex_base == 0: + # Degenerate case: all prices are zero → only exact zero qualifies in_flex = price == 0 else: - # Calculate flex amount using absolute value - flex_amount = abs(criteria.ref_price) * flex_abs + flex_amount = flex_base * flex_abs if criteria.reverse_sort: # Peak price: accept prices >= (ref_price - flex_amount) diff --git a/custom_components/tibber_prices/entity.py b/custom_components/tibber_prices/entity.py index 370386b..d89f2b0 100644 --- a/custom_components/tibber_prices/entity.py +++ b/custom_components/tibber_prices/entity.py @@ -40,7 +40,7 @@ class TibberPricesEntity(CoordinatorEntity[TibberPricesDataUpdateCoordinator]): name=home_name, manufacturer="Tibber", model=translated_model, - serial_number=home_id if home_id else None, + serial_number=home_id or None, configuration_url="https://developer.tibber.com/explorer", ) diff --git a/custom_components/tibber_prices/entity_utils/attributes.py b/custom_components/tibber_prices/entity_utils/attributes.py index ba22ecf..f432c85 100644 --- a/custom_components/tibber_prices/entity_utils/attributes.py +++ b/custom_components/tibber_prices/entity_utils/attributes.py @@ -95,7 +95,7 @@ def add_description_attributes( # noqa: PLR0913, PLR0912 get_entity_description, ) - language = hass.config.language if hass.config.language else "en" + language = hass.config.language or "en" # Build description dict desc_attrs: dict[str, str] = {} @@ -186,7 +186,7 @@ async def async_add_description_attributes( # noqa: PLR0913, PLR0912 async_get_entity_description, ) - language = hass.config.language if hass.config.language else "en" + language = hass.config.language or "en" # Build description dict desc_attrs: dict[str, str] = {} diff --git a/custom_components/tibber_prices/number/core.py b/custom_components/tibber_prices/number/core.py index 445bdab..522741d 100644 --- a/custom_components/tibber_prices/number/core.py +++ b/custom_components/tibber_prices/number/core.py @@ -97,7 +97,7 @@ class TibberPricesConfigNumber(RestoreNumber, NumberEntity): name=home_name, manufacturer="Tibber", model=translated_model, - serial_number=home_id if home_id else None, + serial_number=home_id or None, configuration_url="https://developer.tibber.com/explorer", ) @@ -233,7 +233,7 @@ class TibberPricesConfigNumber(RestoreNumber, NumberEntity): if description: attrs["description"] = description - return attrs if attrs else None + return attrs or None @callback def async_registry_entry_updated(self) -> None: diff --git a/custom_components/tibber_prices/sensor/attributes/__init__.py b/custom_components/tibber_prices/sensor/attributes/__init__.py index f5de39c..4089829 100644 --- a/custom_components/tibber_prices/sensor/attributes/__init__.py +++ b/custom_components/tibber_prices/sensor/attributes/__init__.py @@ -214,7 +214,7 @@ def build_sensor_attributes( ) return None else: - return attributes if attributes else None + return attributes or None def build_extra_state_attributes( # noqa: PLR0913 @@ -287,7 +287,7 @@ def build_extra_state_attributes( # noqa: PLR0913 if sensor_attrs: attributes.update({k: v for k, v in sensor_attrs.items() if k not in ("timestamp", "error")}) - return attributes if attributes else None + return attributes or None # For all other sensors: standard behavior # Start with default timestamp (datetime object - HA serializes automatically) @@ -322,4 +322,4 @@ def build_extra_state_attributes( # noqa: PLR0913 position="end", ) - return attributes if attributes else None + return attributes or None diff --git a/custom_components/tibber_prices/sensor/attributes/future.py b/custom_components/tibber_prices/sensor/attributes/future.py index bba5840..5e88233 100644 --- a/custom_components/tibber_prices/sensor/attributes/future.py +++ b/custom_components/tibber_prices/sensor/attributes/future.py @@ -43,7 +43,7 @@ def add_next_avg_attributes( # noqa: PLR0913 """ # Extract hours from sensor key (e.g., "next_avg_3h" -> 3) try: - hours = int(key.split("_")[-1].replace("h", "")) + hours = int(key.rsplit("_", maxsplit=1)[-1].replace("h", "")) except (ValueError, AttributeError): return diff --git a/custom_components/tibber_prices/sensor/calculators/timing.py b/custom_components/tibber_prices/sensor/calculators/timing.py index 63c02ca..ec2a058 100644 --- a/custom_components/tibber_prices/sensor/calculators/timing.py +++ b/custom_components/tibber_prices/sensor/calculators/timing.py @@ -104,9 +104,9 @@ class TibberPricesTimingCalculator(TibberPricesBaseCalculator): ), "period_duration": lambda: self._calc_period_duration(current_period, next_period), "next_start_time": lambda: next_period.get("start") if next_period else None, - "remaining_minutes": lambda: (self._calc_remaining_minutes(current_period) if current_period else 0), + "remaining_minutes": lambda: self._calc_remaining_minutes(current_period) 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) if next_period else None), + "next_in_minutes": lambda: self._calc_next_in_minutes(next_period) if next_period else None, } calculator = calculators.get(value_type) diff --git a/custom_components/tibber_prices/services/get_chartdata.py b/custom_components/tibber_prices/services/get_chartdata.py index 499f6ff..463d8be 100644 --- a/custom_components/tibber_prices/services/get_chartdata.py +++ b/custom_components/tibber_prices/services/get_chartdata.py @@ -616,7 +616,7 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091 # Mode 'segments': Add NULL points at segment boundaries for clean gaps # Process ALL intervals as one continuous list - no special midnight handling needed filter_field = "rating_level" if rating_level_filter else "level" - filter_values = rating_level_filter if rating_level_filter else level_filter + filter_values = rating_level_filter or level_filter use_rating = rating_level_filter is not None for i in range(len(all_prices) - 1): diff --git a/custom_components/tibber_prices/switch/core.py b/custom_components/tibber_prices/switch/core.py index c2abffd..54487b9 100644 --- a/custom_components/tibber_prices/switch/core.py +++ b/custom_components/tibber_prices/switch/core.py @@ -93,7 +93,7 @@ class TibberPricesConfigSwitch(RestoreEntity, SwitchEntity): name=home_name, manufacturer="Tibber", model=translated_model, - serial_number=home_id if home_id else None, + serial_number=home_id or None, configuration_url="https://developer.tibber.com/explorer", ) @@ -236,7 +236,7 @@ class TibberPricesConfigSwitch(RestoreEntity, SwitchEntity): if description: attrs["description"] = description - return attrs if attrs else None + return attrs or None @callback def async_registry_entry_updated(self) -> None: