refactoring

This commit is contained in:
Julian Pawlowski 2025-05-17 17:39:06 +00:00
parent a4859a9d2e
commit 52cfc4a87f
8 changed files with 329 additions and 128 deletions

View file

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from homeassistant.components.binary_sensor import (
@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.util import dt as dt_util
from .entity import TibberPricesEntity
@ -25,16 +25,23 @@ if TYPE_CHECKING:
from .coordinator import TibberPricesDataUpdateCoordinator
from .data import TibberPricesConfigEntry
from .const import (
CONF_BEST_PRICE_FLEX,
CONF_PEAK_PRICE_FLEX,
DEFAULT_BEST_PRICE_FLEX,
DEFAULT_PEAK_PRICE_FLEX,
)
ENTITY_DESCRIPTIONS = (
BinarySensorEntityDescription(
key="peak_interval",
translation_key="peak_interval",
key="peak_price_period",
translation_key="peak_price_period",
name="Peak Price Interval",
icon="mdi:clock-alert",
),
BinarySensorEntityDescription(
key="best_price_interval",
translation_key="best_price_interval",
key="best_price_period",
translation_key="best_price_period",
name="Best Price Interval",
icon="mdi:clock-check",
),
@ -82,25 +89,70 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
"""Return the appropriate state getter method based on the sensor type."""
key = self.entity_description.key
if key == "peak_interval":
return lambda: self._get_price_threshold_state(threshold_percentage=0.8, high_is_active=True)
if key == "best_price_interval":
return lambda: self._get_price_threshold_state(threshold_percentage=0.2, high_is_active=False)
if key == "peak_price_period":
return self._peak_price_state
if key == "best_price_period":
return self._best_price_state
if key == "connection":
return lambda: True if self.coordinator.data else None
return None
def _get_flex_option(self, option_key: str, default: float) -> float:
"""Get a float option from config entry options or fallback to default. Accepts 0-100."""
options = self.coordinator.config_entry.options
data = self.coordinator.config_entry.data
value = options.get(option_key, data.get(option_key, default))
try:
value = float(value) / 100
except (TypeError, ValueError):
value = default
return value
def _best_price_state(self) -> bool | None:
"""Return True if current price is within +flex% of the day's minimum price."""
price_data = self._get_current_price_data()
if not price_data:
return None
prices, current_price = price_data
min_price = min(prices)
flex = self._get_flex_option(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX)
threshold = min_price * (1 + flex)
return current_price <= threshold
def _peak_price_state(self) -> bool | None:
"""Return True if current price is within -flex% of the day's maximum price."""
price_data = self._get_current_price_data()
if not price_data:
return None
prices, current_price = price_data
max_price = max(prices)
flex = self._get_flex_option(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX)
threshold = max_price * (1 - flex)
return current_price >= threshold
def _get_price_threshold_state(self, *, threshold_percentage: float, high_is_active: bool) -> bool | None:
"""Deprecate: use _best_price_state or _peak_price_state for those sensors."""
price_data = self._get_current_price_data()
if not price_data:
return None
prices, current_price = price_data
threshold_index = int(len(prices) * threshold_percentage)
if high_is_active:
return current_price >= prices[threshold_index]
return current_price <= prices[threshold_index]
def _get_attribute_getter(self) -> Callable | None:
"""Return the appropriate attribute getter method based on the sensor type."""
key = self.entity_description.key
if key == "peak_interval":
return lambda: self._get_price_intervals_attributes(attribute_name="peak_intervals", reverse_sort=True)
if key == "best_price_interval":
return lambda: self._get_price_intervals_attributes(
attribute_name="best_price_intervals", reverse_sort=False
)
if key == "peak_price_period":
return lambda: self._get_price_intervals_attributes(reverse_sort=True)
if key == "best_price_period":
return lambda: self._get_price_intervals_attributes(reverse_sort=False)
return None
@ -130,33 +182,93 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
prices.sort()
return prices, float(current_interval_data["total"])
def _get_price_threshold_state(self, *, threshold_percentage: float, high_is_active: bool) -> bool | None:
def _annotate_period_intervals(
self,
periods: list[list[dict]],
ref_price: float,
avg_price: float,
interval_minutes: int,
) -> list[dict]:
"""Return flattened and annotated intervals with period info and requested properties."""
# Determine reference type for naming
reference_type = None
if self.entity_description.key == "best_price_period":
reference_type = "min"
elif self.entity_description.key == "peak_price_period":
reference_type = "max"
else:
reference_type = "ref"
# Set attribute name suffixes
if reference_type == "min":
diff_key = "price_diff_from_min"
diff_ct_key = "price_diff_from_min_ct"
diff_pct_key = "price_diff_from_min_" + PERCENTAGE
elif reference_type == "max":
diff_key = "price_diff_from_max"
diff_ct_key = "price_diff_from_max_ct"
diff_pct_key = "price_diff_from_max_" + PERCENTAGE
else:
diff_key = "price_diff"
diff_ct_key = "price_diff_ct"
diff_pct_key = "price_diff_" + PERCENTAGE
result = []
period_count = len(periods)
for idx, period in enumerate(periods, 1):
period_start = period[0]["interval_start"] if period else None
period_start_hour = period_start.hour if period_start else None
period_start_minute = period_start.minute if period_start else None
period_start_time = f"{period_start_hour:02d}:{period_start_minute:02d}" if period_start else None
period_end = period[-1]["interval_end"] if period else None
interval_count = len(period)
period_length = interval_count * interval_minutes
periods_remaining = len(periods) - idx
for interval_idx, interval in enumerate(period, 1):
interval_copy = interval.copy()
interval_remaining = interval_count - interval_idx
# Compose new dict with period-related keys first, then interval timing, then price info
new_interval = {
"period_start": period_start,
"period_end": period_end,
"hour": period_start_hour,
"minute": period_start_minute,
"time": period_start_time,
"period_length_minute": period_length,
"period_remaining_minute_after_interval": interval_remaining * interval_minutes,
"periods_total": period_count,
"periods_remaining": periods_remaining,
"interval_total": interval_count,
"interval_remaining": interval_remaining,
"interval_position": interval_idx,
}
# Add interval timing
new_interval["interval_start"] = interval_copy.pop("interval_start", None)
new_interval["interval_end"] = interval_copy.pop("interval_end", None)
# Add hour, minute, time, price if present in interval_copy
for k in ("interval_hour", "interval_minute", "interval_time", "price"):
if k in interval_copy:
new_interval[k] = interval_copy.pop(k)
# Add the rest of the interval info (e.g. price_ct, price_difference_*, etc.)
new_interval.update(interval_copy)
new_interval["price_ct"] = round(new_interval["price"] * 100, 2)
price_diff = new_interval["price"] - ref_price
new_interval[diff_key] = round(price_diff, 4)
new_interval[diff_ct_key] = round(price_diff * 100, 2)
price_diff_percent = ((new_interval["price"] - ref_price) / ref_price) * 100 if ref_price != 0 else 0.0
new_interval[diff_pct_key] = round(price_diff_percent, 2)
# Add difference to average price of the day (avg_price is now passed in)
avg_diff = new_interval["price"] - avg_price
new_interval["price_diff_from_avg"] = round(avg_diff, 4)
new_interval["price_diff_from_avg_ct"] = round(avg_diff * 100, 2)
avg_diff_percent = ((new_interval["price"] - avg_price) / avg_price) * 100 if avg_price != 0 else 0.0
new_interval["price_diff_from_avg_" + PERCENTAGE] = round(avg_diff_percent, 2)
result.append(new_interval)
return result
def _get_price_intervals_attributes(self, *, reverse_sort: bool) -> dict | None:
"""
Determine if current price is above/below threshold.
Get price interval attributes with support for 15-minute intervals and period grouping.
Args:
threshold_percentage: The percentage point in the sorted list (0.0-1.0)
high_is_active: If True, value >= threshold is active, otherwise value <= threshold is active
"""
price_data = self._get_current_price_data()
if not price_data:
return None
prices, current_price = price_data
threshold_index = int(len(prices) * threshold_percentage)
if high_is_active:
return current_price >= prices[threshold_index]
return current_price <= prices[threshold_index]
def _get_price_intervals_attributes(self, *, attribute_name: str, reverse_sort: bool) -> dict | None:
"""
Get price interval attributes with support for 15-minute intervals.
Args:
attribute_name: The attribute name to use in the result dictionary
reverse_sort: Whether to sort prices in reverse (high to low)
Returns:
@ -172,51 +284,91 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
if not today_prices:
return None
# Detect the granularity of the data
interval_minutes = detect_interval_granularity(today_prices)
# Build a list of price data with timestamps and values
price_intervals = []
# Use entity type to determine flex and logic, but always use 'price_intervals' as attribute name
if reverse_sort is False: # best_price_period entity
flex = self._get_flex_option(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX)
prices = [float(p["total"]) for p in today_prices]
min_price = min(prices)
def in_range(price: float) -> bool:
return price <= min_price * (1 + flex)
ref_price = min_price
elif reverse_sort is True: # peak_price_period entity
flex = self._get_flex_option(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX)
prices = [float(p["total"]) for p in today_prices]
max_price = max(prices)
def in_range(price: float) -> bool:
return price >= max_price * (1 - flex)
ref_price = max_price
else:
return None
# Calculate average price for the day (all intervals, not just periods)
all_prices = [float(p["total"]) for p in today_prices]
avg_price = sum(all_prices) / len(all_prices) if all_prices else 0.0
# Build intervals with period grouping
periods = []
current_period = []
for price_data in today_prices:
starts_at = dt_util.parse_datetime(price_data["startsAt"])
if starts_at is None:
continue
starts_at = dt_util.as_local(starts_at)
price_intervals.append(
price = float(price_data["total"])
if in_range(price):
current_period.append(
{
"starts_at": starts_at,
"price": float(price_data["total"]),
"hour": starts_at.hour,
"minute": starts_at.minute,
"interval_hour": starts_at.hour,
"interval_minute": starts_at.minute,
"interval_time": f"{starts_at.hour:02d}:{starts_at.minute:02d}",
"price": price,
"interval_start": starts_at,
# interval_end will be filled later
}
)
elif current_period:
periods.append(current_period)
current_period = []
if current_period:
periods.append(current_period)
# Sort by price (high to low for peak, low to high for best)
sorted_intervals = sorted(price_intervals, key=lambda x: x["price"], reverse=reverse_sort)[:5]
# Add interval_end to each interval (next interval's start or None)
for period in periods:
for idx, interval in enumerate(period):
if idx + 1 < len(period):
interval["interval_end"] = period[idx + 1]["interval_start"]
else:
# Try to estimate end as start + interval_minutes
interval["interval_end"] = interval["interval_start"] + timedelta(minutes=interval_minutes)
# Format the result based on granularity
hourly_interval_minutes = 60
result = []
for interval in sorted_intervals:
if interval_minutes < hourly_interval_minutes: # More granular than hourly
result.append(
{
"hour": interval["hour"],
"minute": interval["minute"],
"time": f"{interval['hour']:02d}:{interval['minute']:02d}",
"price": interval["price"],
}
)
else: # Hourly data (for backward compatibility)
result.append(
{
"hour": interval["hour"],
"price": interval["price"],
}
)
result = self._annotate_period_intervals(periods, ref_price, avg_price, interval_minutes)
return {attribute_name: result}
# Find the current or next interval (by time) from the annotated result
now = dt_util.now()
current_interval = None
for interval in result:
start = interval.get("interval_start")
end = interval.get("interval_end")
if start and end and start <= now < end:
current_interval = interval.copy()
break
else:
# If no current interval, show the next period's first interval (if available)
for interval in result:
start = interval.get("interval_start")
if start and start > now:
current_interval = interval.copy()
break
attributes = {**current_interval} if current_interval else {}
attributes["intervals"] = result
return attributes
def _get_price_hours_attributes(self, *, attribute_name: str, reverse_sort: bool) -> dict | None:
"""Get price hours attributes."""

View file

@ -17,8 +17,12 @@ from .api import (
TibberPricesApiClientError,
)
from .const import (
CONF_BEST_PRICE_FLEX,
CONF_EXTENDED_DESCRIPTIONS,
CONF_PEAK_PRICE_FLEX,
DEFAULT_BEST_PRICE_FLEX,
DEFAULT_EXTENDED_DESCRIPTIONS,
DEFAULT_PEAK_PRICE_FLEX,
DOMAIN,
LOGGER,
)
@ -87,6 +91,28 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_EXTENDED_DESCRIPTIONS,
default=(user_input or {}).get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
): selector.BooleanSelector(),
vol.Optional(
CONF_BEST_PRICE_FLEX,
default=(user_input or {}).get(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX),
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=20,
step=1,
mode=selector.NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_PEAK_PRICE_FLEX,
default=(user_input or {}).get(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX),
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=20,
step=1,
mode=selector.NumberSelectorMode.SLIDER,
),
),
},
),
errors=_errors,
@ -105,10 +131,9 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow):
"""Tibber Prices config flow options handler."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
def __init__(self, _: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
super().__init__()
self.config_entry = config_entry
async def async_step_init(self, user_input: dict | None = None) -> config_entries.ConfigFlowResult:
"""Manage the options."""
@ -162,6 +187,38 @@ class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow):
self.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
),
): selector.BooleanSelector(),
vol.Optional(
CONF_BEST_PRICE_FLEX,
default=int(
self.config_entry.options.get(
CONF_BEST_PRICE_FLEX,
self.config_entry.data.get(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX),
)
),
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=100,
step=1,
mode=selector.NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_PEAK_PRICE_FLEX,
default=int(
self.config_entry.options.get(
CONF_PEAK_PRICE_FLEX,
self.config_entry.data.get(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX),
)
),
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=100,
step=1,
mode=selector.NumberSelectorMode.SLIDER,
),
),
}
return self.async_show_form(

View file

@ -16,6 +16,8 @@ VERSION = "0.1.0"
DOMAIN = "tibber_prices"
CONF_ACCESS_TOKEN = "access_token" # noqa: S105
CONF_EXTENDED_DESCRIPTIONS = "extended_descriptions"
CONF_BEST_PRICE_FLEX = "best_price_flex"
CONF_PEAK_PRICE_FLEX = "peak_price_flex"
ATTRIBUTION = "Data provided by Tibber"
@ -25,6 +27,8 @@ SCAN_INTERVAL = 60 * 5 # 5 minutes
# Integration name should match manifest.json
DEFAULT_NAME = "Tibber Price Information & Ratings"
DEFAULT_EXTENDED_DESCRIPTIONS = False
DEFAULT_BEST_PRICE_FLEX = 5 # 5% flexibility for best price (user-facing, percent)
DEFAULT_PEAK_PRICE_FLEX = 5 # 5% flexibility for peak price (user-facing, percent)
# Price level constants
PRICE_LEVEL_NORMAL = "NORMAL"

View file

@ -116,30 +116,18 @@
}
},
"binary_sensor": {
"peak_interval": {
"name": "Spitzenpreis-Intervall",
"peak_price_period": {
"name": "Spitzenpreis-Periode",
"description": "Ob das aktuelle Intervall zu den teuersten des Tages gehört",
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den oberen 20% der heutigen Preise liegt",
"usage_tips": "Nutze dies, um den Betrieb von Geräten mit hohem Verbrauch während teurer Intervalle zu vermeiden"
},
"best_price_interval": {
"name": "Bestpreis-Intervall",
"best_price_period": {
"name": "Bestpreis-Periode",
"description": "Ob das aktuelle Intervall zu den günstigsten des Tages gehört",
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den unteren 20% der heutigen Preise liegt",
"usage_tips": "Nutze dies, um Geräte mit hohem Verbrauch während der günstigsten Intervalle zu betreiben"
},
"peak_hour": {
"name": "Spitzenstunde",
"description": "Ob die aktuelle Stunde zu den teuersten des Tages gehört",
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den oberen 20% der heutigen Preise liegt",
"usage_tips": "Nutze dies, um den Betrieb von Geräten mit hohem Verbrauch während teurer Stunden zu vermeiden"
},
"best_price_hour": {
"name": "Beste-Preis-Stunde",
"description": "Ob die aktuelle Stunde zu den günstigsten des Tages gehört",
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den unteren 20% der heutigen Preise liegt",
"usage_tips": "Nutze dies, um Geräte mit hohem Verbrauch während der günstigsten Stunden zu betreiben"
},
"connection": {
"name": "Tibber API-Verbindung",
"description": "Ob die Verbindung zur Tibber API funktioniert",

View file

@ -116,30 +116,18 @@
}
},
"binary_sensor": {
"peak_interval": {
"name": "Peak Price Interval",
"peak_price_period": {
"name": "Peak Price Periode",
"description": "Whether the current interval is among the most expensive of the day",
"long_description": "Turns on when the current price is in the top 20% of today's prices",
"usage_tips": "Use this to avoid running high-consumption appliances during expensive intervals"
},
"best_price_interval": {
"name": "Best Price Interval",
"best_price_period": {
"name": "Best Price Periode",
"description": "Whether the current interval is among the cheapest of the day",
"long_description": "Turns on when the current price is in the bottom 20% of today's prices",
"usage_tips": "Use this to run high-consumption appliances during the cheapest intervals"
},
"peak_hour": {
"name": "Peak Hour",
"description": "Whether the current hour is among the most expensive of the day",
"long_description": "Turns on when the current price is in the top 20% of today's prices",
"usage_tips": "Use this to avoid running high-consumption appliances during expensive hours"
},
"best_price_hour": {
"name": "Best Price Hour",
"description": "Whether the current hour is among the cheapest of the day",
"long_description": "Turns on when the current price is in the bottom 20% of today's prices",
"usage_tips": "Use this to run high-consumption appliances during the cheapest hours"
},
"connection": {
"name": "Tibber API Connection",
"description": "Whether the connection to the Tibber API is working",

View file

@ -13,7 +13,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import CURRENCY_CENT, CURRENCY_EURO, PERCENTAGE, EntityCategory, UnitOfPower, UnitOfTime
from homeassistant.const import CURRENCY_EURO, PERCENTAGE, EntityCategory, UnitOfPower, UnitOfTime
from homeassistant.util import dt as dt_util
from .const import (
@ -36,7 +36,7 @@ if TYPE_CHECKING:
from .coordinator import TibberPricesDataUpdateCoordinator
from .data import TibberPricesConfigEntry
PRICE_UNIT_CENT = CURRENCY_CENT + "/" + UnitOfPower.KILO_WATT + UnitOfTime.HOURS
PRICE_UNIT_CENT = "ct/" + UnitOfPower.KILO_WATT + UnitOfTime.HOURS
PRICE_UNIT_EURO = CURRENCY_EURO + "/" + UnitOfPower.KILO_WATT + UnitOfTime.HOURS
HOURS_IN_DAY = 24
LAST_HOUR_OF_DAY = 23

View file

@ -5,7 +5,9 @@
"description": "Richte Tibber Preisinformationen & Bewertungen ein. Um ein API-Zugriffstoken zu generieren, besuche developer.tibber.com.",
"data": {
"access_token": "API-Zugriffstoken",
"extended_descriptions": "Erweiterte Beschreibungen in Entitätsattributen anzeigen"
"extended_descriptions": "Erweiterte Beschreibungen in Entitätsattributen anzeigen",
"best_price_flex": "Flexibilität für Bestpreis (%)",
"peak_price_flex": "Flexibilität für Spitzenpreis (%)"
},
"title": "Tibber Preisinformationen & Bewertungen"
}
@ -29,7 +31,9 @@
"description": "Konfiguriere Optionen für Tibber Preisinformationen & Bewertungen",
"data": {
"access_token": "Tibber Zugangstoken",
"extended_descriptions": "Erweiterte Beschreibungen in Entitätsattributen anzeigen"
"extended_descriptions": "Erweiterte Beschreibungen in Entitätsattributen anzeigen",
"best_price_flex": "Flexibilität für Bestpreis (%)",
"peak_price_flex": "Flexibilität für Spitzenpreis (%)"
}
}
},
@ -83,11 +87,11 @@
}
},
"binary_sensor": {
"peak_hour": {
"name": "Spitzenstunde"
"peak_price_period": {
"name": "Spitzenperiode"
},
"best_price_hour": {
"name": "Beste-Preis-Stunde"
"best_price_period": {
"name": "Best-Preis-Periode"
},
"connection": {
"name": "Tibber API-Verbindung"

View file

@ -5,7 +5,9 @@
"description": "Set up Tibber Price Information & Ratings. To generate an API access token, visit developer.tibber.com.",
"data": {
"access_token": "API access token",
"extended_descriptions": "Show extended descriptions in entity attributes"
"extended_descriptions": "Show extended descriptions in entity attributes",
"best_price_flex": "Best Price Flexibility (%)",
"peak_price_flex": "Peak Price Flexibility (%)"
},
"title": "Tibber Price Information & Ratings"
}
@ -20,7 +22,9 @@
"abort": {
"already_configured": "Integration is already configured",
"entry_not_found": "Tibber configuration entry not found."
}
},
"best_price_flex": "Best Price Flexibility (%)",
"peak_price_flex": "Peak Price Flexibility (%)"
},
"options": {
"step": {
@ -29,7 +33,9 @@
"description": "Configure options for Tibber Price Information & Ratings",
"data": {
"access_token": "Tibber Access Token",
"extended_descriptions": "Show extended descriptions in entity attributes"
"extended_descriptions": "Show extended descriptions in entity attributes",
"best_price_flex": "Best Price Flexibility (%)",
"peak_price_flex": "Peak Price Flexibility (%)"
}
}
},
@ -41,7 +47,9 @@
},
"abort": {
"entry_not_found": "Tibber configuration entry not found."
}
},
"best_price_flex": "Best Price Flexibility (%)",
"peak_price_flex": "Peak Price Flexibility (%)"
},
"entity": {
"sensor": {
@ -98,11 +106,11 @@
}
},
"binary_sensor": {
"peak_hour": {
"name": "Peak Hour"
"peak_price_period": {
"name": "Peak Price Period"
},
"best_price_hour": {
"name": "Best Price Hour"
"best_price_period": {
"name": "Best Price Period"
},
"connection": {
"name": "Tibber API Connection"