mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
feat(sensor): migrate chart_data_export from binary_sensor to sensor platform
Migrated chart_data_export from binary_sensor to sensor to enable
compatibility with dashboard integrations (ApexCharts, etc.) that
require sensor entities for data selection.
Changes:
- Moved chart_data_export from binary_sensor/ to sensor/ platform
- Changed from boolean state (ON/OFF) to ENUM states ("pending", "ready", "error")
- Maintained all functionality: service call, attribute structure, caching
- Updated translations in all 5 languages (de, en, nb, nl, sv)
- Updated user documentation (sensors.md, services.md)
- Removed all chart_data_export code from binary_sensor platform
Technical details:
- State: "pending" (before first call), "ready" (data available), "error" (service failed)
- Attributes: timestamp + error (metadata) → descriptions → service response data
- Cache (_chart_data_response) bridges async service call and sync property access
- Service call: Triggered on async_added_to_hass() and async_update()
Impact: Dashboard integrations can now select chart_data_export sensor
in their entity pickers. No breaking changes for existing users - entity ID
changes from binary_sensor.* to sensor.*, but functionality identical.
This commit is contained in:
parent
e17f59c283
commit
ef983d0a99
16 changed files with 280 additions and 349 deletions
|
|
@ -274,7 +274,7 @@ def build_final_attributes_simple(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def build_async_extra_state_attributes( # noqa: PLR0913, PLR0912
|
async def build_async_extra_state_attributes( # noqa: PLR0913
|
||||||
entity_key: str,
|
entity_key: str,
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
@ -302,28 +302,8 @@ async def build_async_extra_state_attributes( # noqa: PLR0913, PLR0912
|
||||||
"""
|
"""
|
||||||
attributes = {}
|
attributes = {}
|
||||||
|
|
||||||
# For chart_data_export: Add metadata first, descriptions next, service data last
|
# Add dynamic attributes first
|
||||||
# For other sensors: Follow normal order (dynamic_attrs first, then descriptions)
|
|
||||||
is_chart_export = entity_key == "chart_data_export"
|
|
||||||
|
|
||||||
# Extract metadata and service data for chart_data_export
|
|
||||||
chart_metadata = {}
|
|
||||||
chart_service_data = {}
|
|
||||||
if is_chart_export and dynamic_attrs:
|
|
||||||
# Separate metadata (timestamp, error) from service data
|
|
||||||
for key, value in dynamic_attrs.items():
|
|
||||||
if key in ("timestamp", "error"):
|
|
||||||
chart_metadata[key] = value
|
|
||||||
else:
|
|
||||||
chart_service_data[key] = value
|
|
||||||
|
|
||||||
# Add dynamic attributes in correct order
|
|
||||||
if dynamic_attrs:
|
if dynamic_attrs:
|
||||||
if is_chart_export:
|
|
||||||
# For chart_data_export: Start with metadata only
|
|
||||||
attributes.update(chart_metadata)
|
|
||||||
else:
|
|
||||||
# For other sensors: Add all dynamic attributes first
|
|
||||||
# Copy and remove internal fields before exposing to user
|
# Copy and remove internal fields before exposing to user
|
||||||
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
||||||
attributes.update(clean_attrs)
|
attributes.update(clean_attrs)
|
||||||
|
|
@ -377,14 +357,10 @@ async def build_async_extra_state_attributes( # noqa: PLR0913, PLR0912
|
||||||
if usage_tips:
|
if usage_tips:
|
||||||
attributes["usage_tips"] = usage_tips
|
attributes["usage_tips"] = usage_tips
|
||||||
|
|
||||||
# For chart_data_export: Add service data at the END (after descriptions)
|
|
||||||
if is_chart_export and chart_service_data:
|
|
||||||
attributes.update(chart_service_data)
|
|
||||||
|
|
||||||
return attributes if attributes else None
|
return attributes if attributes else None
|
||||||
|
|
||||||
|
|
||||||
def build_sync_extra_state_attributes( # noqa: PLR0913, PLR0912
|
def build_sync_extra_state_attributes( # noqa: PLR0913
|
||||||
entity_key: str,
|
entity_key: str,
|
||||||
translation_key: str | None,
|
translation_key: str | None,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
@ -412,28 +388,8 @@ def build_sync_extra_state_attributes( # noqa: PLR0913, PLR0912
|
||||||
"""
|
"""
|
||||||
attributes = {}
|
attributes = {}
|
||||||
|
|
||||||
# For chart_data_export: Add metadata first, descriptions next, service data last
|
# Add dynamic attributes first
|
||||||
# For other sensors: Follow normal order (dynamic_attrs first, then descriptions)
|
|
||||||
is_chart_export = entity_key == "chart_data_export"
|
|
||||||
|
|
||||||
# Extract metadata and service data for chart_data_export
|
|
||||||
chart_metadata = {}
|
|
||||||
chart_service_data = {}
|
|
||||||
if is_chart_export and dynamic_attrs:
|
|
||||||
# Separate metadata (timestamp, error) from service data
|
|
||||||
for key, value in dynamic_attrs.items():
|
|
||||||
if key in ("timestamp", "error"):
|
|
||||||
chart_metadata[key] = value
|
|
||||||
else:
|
|
||||||
chart_service_data[key] = value
|
|
||||||
|
|
||||||
# Add dynamic attributes in correct order
|
|
||||||
if dynamic_attrs:
|
if dynamic_attrs:
|
||||||
if is_chart_export:
|
|
||||||
# For chart_data_export: Start with metadata only
|
|
||||||
attributes.update(chart_metadata)
|
|
||||||
else:
|
|
||||||
# For other sensors: Add all dynamic attributes first
|
|
||||||
# Copy and remove internal fields before exposing to user
|
# Copy and remove internal fields before exposing to user
|
||||||
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
||||||
attributes.update(clean_attrs)
|
attributes.update(clean_attrs)
|
||||||
|
|
@ -484,8 +440,4 @@ def build_sync_extra_state_attributes( # noqa: PLR0913, PLR0912
|
||||||
if usage_tips:
|
if usage_tips:
|
||||||
attributes["usage_tips"] = usage_tips
|
attributes["usage_tips"] = usage_tips
|
||||||
|
|
||||||
# For chart_data_export: Add service data at the END (after descriptions)
|
|
||||||
if is_chart_export and chart_service_data:
|
|
||||||
attributes.update(chart_service_data)
|
|
||||||
|
|
||||||
return attributes if attributes else None
|
return attributes if attributes else None
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,6 @@ from __future__ import annotations
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import CONF_CHART_DATA_CONFIG, DOMAIN
|
|
||||||
from custom_components.tibber_prices.coordinator import TIME_SENSITIVE_ENTITY_KEYS
|
from custom_components.tibber_prices.coordinator import TIME_SENSITIVE_ENTITY_KEYS
|
||||||
from custom_components.tibber_prices.entity import TibberPricesEntity
|
from custom_components.tibber_prices.entity import TibberPricesEntity
|
||||||
from custom_components.tibber_prices.entity_utils import get_binary_sensor_icon
|
from custom_components.tibber_prices.entity_utils import get_binary_sensor_icon
|
||||||
|
|
@ -52,8 +49,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
self._state_getter: Callable | None = self._get_state_getter()
|
self._state_getter: Callable | None = self._get_state_getter()
|
||||||
self._attribute_getter: Callable | None = self._get_attribute_getter()
|
self._attribute_getter: Callable | None = self._get_attribute_getter()
|
||||||
self._time_sensitive_remove_listener: Callable | None = None
|
self._time_sensitive_remove_listener: Callable | None = None
|
||||||
self._chart_data_last_update = None # Track last service call timestamp
|
|
||||||
self._chart_data_error = None # Track last service call error
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
|
|
@ -65,10 +60,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
self._handle_time_sensitive_update
|
self._handle_time_sensitive_update
|
||||||
)
|
)
|
||||||
|
|
||||||
# For chart_data_export, trigger initial service call
|
|
||||||
if self.entity_description.key == "chart_data_export":
|
|
||||||
await self._refresh_chart_data()
|
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""When entity will be removed from hass."""
|
"""When entity will be removed from hass."""
|
||||||
await super().async_will_remove_from_hass()
|
await super().async_will_remove_from_hass()
|
||||||
|
|
@ -94,7 +85,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
"tomorrow_data_available": self._tomorrow_data_available_state,
|
"tomorrow_data_available": self._tomorrow_data_available_state,
|
||||||
"has_ventilation_system": self._has_ventilation_system_state,
|
"has_ventilation_system": self._has_ventilation_system_state,
|
||||||
"realtime_consumption_enabled": self._realtime_consumption_enabled_state,
|
"realtime_consumption_enabled": self._realtime_consumption_enabled_state,
|
||||||
"chart_data_export": self._chart_data_export_state,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return state_getters.get(key)
|
return state_getters.get(key)
|
||||||
|
|
@ -184,79 +174,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
value = features.get("realTimeConsumptionEnabled")
|
value = features.get("realTimeConsumptionEnabled")
|
||||||
return value if isinstance(value, bool) else None
|
return value if isinstance(value, bool) else None
|
||||||
|
|
||||||
def _chart_data_export_state(self) -> bool | None:
|
|
||||||
"""Return True if chart data export was successful."""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Try to fetch chart data - state is ON if successful
|
|
||||||
# Note: This is called in property context, so we can't use async
|
|
||||||
# We'll check if data was cached from last async call
|
|
||||||
chart_data = self._get_cached_chart_data()
|
|
||||||
return chart_data is not None
|
|
||||||
|
|
||||||
def _get_cached_chart_data(self) -> dict | None:
|
|
||||||
"""Get cached chart data from last service call."""
|
|
||||||
# Store service response in instance variable for reuse
|
|
||||||
if not hasattr(self, "_chart_data_cache"):
|
|
||||||
self._chart_data_cache = None
|
|
||||||
return self._chart_data_cache
|
|
||||||
|
|
||||||
async def _call_chartdata_service_async(self) -> dict | None:
|
|
||||||
"""Call get_chartdata service with user-configured YAML (async)."""
|
|
||||||
# Get user-configured YAML
|
|
||||||
yaml_config = self.coordinator.config_entry.options.get(CONF_CHART_DATA_CONFIG, "")
|
|
||||||
|
|
||||||
# Parse YAML if provided, otherwise use empty dict (service defaults)
|
|
||||||
service_params = {}
|
|
||||||
if yaml_config and yaml_config.strip():
|
|
||||||
try:
|
|
||||||
parsed = yaml.safe_load(yaml_config)
|
|
||||||
# Ensure we have a dict (yaml.safe_load can return str, int, etc.)
|
|
||||||
if isinstance(parsed, dict):
|
|
||||||
service_params = parsed
|
|
||||||
else:
|
|
||||||
self.coordinator.logger.warning(
|
|
||||||
"YAML configuration must be a dictionary, got %s. Using service defaults.",
|
|
||||||
type(parsed).__name__,
|
|
||||||
extra={"entity": self.entity_description.key},
|
|
||||||
)
|
|
||||||
service_params = {}
|
|
||||||
except yaml.YAMLError as err:
|
|
||||||
self.coordinator.logger.warning(
|
|
||||||
"Invalid chart data YAML configuration: %s. Using service defaults.",
|
|
||||||
err,
|
|
||||||
extra={"entity": self.entity_description.key},
|
|
||||||
)
|
|
||||||
service_params = {} # Fall back to service defaults
|
|
||||||
|
|
||||||
# Add required entry_id parameter
|
|
||||||
service_params["entry_id"] = self.coordinator.config_entry.entry_id
|
|
||||||
|
|
||||||
# Call get_chartdata service using official HA service system
|
|
||||||
try:
|
|
||||||
response = await self.hass.services.async_call(
|
|
||||||
DOMAIN,
|
|
||||||
"get_chartdata",
|
|
||||||
service_params,
|
|
||||||
blocking=True,
|
|
||||||
return_response=True,
|
|
||||||
)
|
|
||||||
except Exception as ex:
|
|
||||||
self.coordinator.logger.exception(
|
|
||||||
"Chart data service call failed",
|
|
||||||
extra={"entity": self.entity_description.key},
|
|
||||||
)
|
|
||||||
self._chart_data_cache = None
|
|
||||||
self._chart_data_last_update = dt_util.now()
|
|
||||||
self._chart_data_error = str(ex)
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
self._chart_data_cache = response
|
|
||||||
self._chart_data_last_update = dt_util.now()
|
|
||||||
self._chart_data_error = None
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _get_tomorrow_data_available_attributes(self) -> dict | None:
|
def _get_tomorrow_data_available_attributes(self) -> dict | None:
|
||||||
"""Return attributes for tomorrow_data_available binary sensor."""
|
"""Return attributes for tomorrow_data_available binary sensor."""
|
||||||
return get_tomorrow_data_available_attributes(self.coordinator.data)
|
return get_tomorrow_data_available_attributes(self.coordinator.data)
|
||||||
|
|
@ -271,58 +188,9 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
return lambda: get_price_intervals_attributes(self.coordinator.data, reverse_sort=False)
|
return lambda: get_price_intervals_attributes(self.coordinator.data, reverse_sort=False)
|
||||||
if key == "tomorrow_data_available":
|
if key == "tomorrow_data_available":
|
||||||
return self._get_tomorrow_data_available_attributes
|
return self._get_tomorrow_data_available_attributes
|
||||||
if key == "chart_data_export":
|
|
||||||
return self._get_chart_data_export_attributes
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_chart_data_export_attributes(self) -> dict[str, object] | None:
|
|
||||||
"""
|
|
||||||
Return chart data from service call as attributes with metadata.
|
|
||||||
|
|
||||||
Strategy to avoid attribute name collisions:
|
|
||||||
- If service returns dict with SINGLE top-level key → use directly
|
|
||||||
- If service returns dict with MULTIPLE top-level keys → wrap in {"data": {...}}
|
|
||||||
- If service returns array/primitive → wrap in {"data": <response>}
|
|
||||||
|
|
||||||
Attribute order: timestamp, error (if any), descriptions, service data (at the end).
|
|
||||||
"""
|
|
||||||
chart_data = self._get_cached_chart_data()
|
|
||||||
|
|
||||||
# Build base attributes with metadata
|
|
||||||
# timestamp = when service was last called (not current interval)
|
|
||||||
attributes: dict[str, object] = {
|
|
||||||
"timestamp": self._chart_data_last_update.isoformat() if self._chart_data_last_update else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add error message if service call failed
|
|
||||||
if self._chart_data_error:
|
|
||||||
attributes["error"] = self._chart_data_error
|
|
||||||
|
|
||||||
# Note: descriptions will be added by build_async_extra_state_attributes
|
|
||||||
# and will appear before service data because we return attributes first,
|
|
||||||
# then they get merged with descriptions, then service data is appended
|
|
||||||
|
|
||||||
if not chart_data:
|
|
||||||
# No data - only metadata (timestamp, error)
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
# Service data goes at the END - append after metadata
|
|
||||||
# If response is a dict with multiple top-level keys, wrap it
|
|
||||||
# to avoid collision with our own attributes (timestamp, error, etc.)
|
|
||||||
if isinstance(chart_data, dict):
|
|
||||||
if len(chart_data) > 1:
|
|
||||||
# Multiple keys → wrap to prevent collision
|
|
||||||
attributes["data"] = chart_data
|
|
||||||
else:
|
|
||||||
# Single key → safe to merge directly
|
|
||||||
attributes.update(chart_data)
|
|
||||||
else:
|
|
||||||
# If response is array/list/primitive, wrap it in "data" key
|
|
||||||
attributes["data"] = chart_data
|
|
||||||
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
|
|
@ -331,20 +199,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
# 1. Initial sensor activation (async_added_to_hass)
|
# 1. Initial sensor activation (async_added_to_hass)
|
||||||
# 2. Config changes via Options Flow (triggers re-add)
|
# 2. Config changes via Options Flow (triggers re-add)
|
||||||
# Hourly coordinator updates don't change the chart data content.
|
# Hourly coordinator updates don't change the chart data content.
|
||||||
super()._handle_coordinator_update()
|
|
||||||
|
|
||||||
async def _refresh_chart_data(self) -> None:
|
|
||||||
"""
|
|
||||||
Refresh chart data by calling service.
|
|
||||||
|
|
||||||
Called only on:
|
|
||||||
- Initial sensor activation (async_added_to_hass)
|
|
||||||
- Config changes via Options Flow (triggers re-add → async_added_to_hass)
|
|
||||||
|
|
||||||
NOT called on routine coordinator updates to avoid unnecessary service calls.
|
|
||||||
"""
|
|
||||||
await self._call_chartdata_service_async()
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool | None:
|
def is_on(self) -> bool | None:
|
||||||
|
|
@ -417,19 +271,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
async def async_extra_state_attributes(self) -> dict | None:
|
async def async_extra_state_attributes(self) -> dict | None:
|
||||||
"""Return additional state attributes asynchronously."""
|
"""Return additional state attributes asynchronously."""
|
||||||
try:
|
try:
|
||||||
# For chart_data_export, use custom attribute builder with descriptions
|
|
||||||
if self.entity_description.key == "chart_data_export":
|
|
||||||
chart_attrs = self._get_chart_data_export_attributes()
|
|
||||||
# Add descriptions like other sensors
|
|
||||||
return await build_async_extra_state_attributes(
|
|
||||||
self.entity_description.key,
|
|
||||||
self.entity_description.translation_key,
|
|
||||||
self.hass,
|
|
||||||
config_entry=self.coordinator.config_entry,
|
|
||||||
dynamic_attrs=chart_attrs,
|
|
||||||
is_on=self.is_on,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the dynamic attributes if the getter is available
|
# Get the dynamic attributes if the getter is available
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
@ -462,19 +303,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
def extra_state_attributes(self) -> dict | None:
|
def extra_state_attributes(self) -> dict | None:
|
||||||
"""Return additional state attributes synchronously."""
|
"""Return additional state attributes synchronously."""
|
||||||
try:
|
try:
|
||||||
# For chart_data_export, use custom attribute builder with descriptions
|
|
||||||
if self.entity_description.key == "chart_data_export":
|
|
||||||
chart_attrs = self._get_chart_data_export_attributes()
|
|
||||||
# Add descriptions like other sensors
|
|
||||||
return build_sync_extra_state_attributes(
|
|
||||||
self.entity_description.key,
|
|
||||||
self.entity_description.translation_key,
|
|
||||||
self.hass,
|
|
||||||
config_entry=self.coordinator.config_entry,
|
|
||||||
dynamic_attrs=chart_attrs,
|
|
||||||
is_on=self.is_on,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the dynamic attributes if the getter is available
|
# Get the dynamic attributes if the getter is available
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
@ -507,7 +335,3 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
"""Force a refresh when homeassistant.update_entity is called."""
|
"""Force a refresh when homeassistant.update_entity is called."""
|
||||||
# Always refresh coordinator data
|
# Always refresh coordinator data
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
# For chart_data_export, also refresh the service call
|
|
||||||
if self.entity_description.key == "chart_data_export":
|
|
||||||
await self._refresh_chart_data()
|
|
||||||
|
|
|
||||||
|
|
@ -62,13 +62,4 @@ ENTITY_DESCRIPTIONS = (
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
BinarySensorEntityDescription(
|
|
||||||
key="chart_data_export",
|
|
||||||
translation_key="chart_data_export",
|
|
||||||
name="Chart Data Export",
|
|
||||||
icon="mdi:database-export",
|
|
||||||
device_class=BinarySensorDeviceClass.CONNECTIVITY, # ON = data available
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False, # Opt-in
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,11 @@
|
||||||
"description": "Status deines Tibber-Abonnements",
|
"description": "Status deines Tibber-Abonnements",
|
||||||
"long_description": "Zeigt, ob dein Tibber-Abonnement derzeit aktiv ist, beendet wurde oder auf Aktivierung wartet. Ein Status 'Aktiv' bedeutet, dass du aktiv Strom über Tibber beziehst.",
|
"long_description": "Zeigt, ob dein Tibber-Abonnement derzeit aktiv ist, beendet wurde oder auf Aktivierung wartet. Ein Status 'Aktiv' bedeutet, dass du aktiv Strom über Tibber beziehst.",
|
||||||
"usage_tips": "Nutze dies zur Überwachung deines Abonnementstatus. Richte Benachrichtigungen ein, wenn sich der Status von 'Aktiv' ändert, um einen unterbrechungsfreien Service sicherzustellen."
|
"usage_tips": "Nutze dies zur Überwachung deines Abonnementstatus. Richte Benachrichtigungen ein, wenn sich der Status von 'Aktiv' ändert, um einen unterbrechungsfreien Service sicherzustellen."
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"description": "Datenexport für Dashboard-Integrationen",
|
||||||
|
"long_description": "Dieser Sensor ruft den get_chartdata-Service mit deiner konfigurierten YAML-Konfiguration auf und stellt das Ergebnis als Entity-Attribute bereit. Der Status zeigt 'ready' wenn Daten verfügbar sind, 'error' bei Fehlern, oder 'pending' vor dem ersten Aufruf. Perfekt für Dashboard-Integrationen wie ApexCharts, die Preisdaten aus Entity-Attributen lesen.",
|
||||||
|
"usage_tips": "Konfiguriere die YAML-Parameter in den Integrationsoptionen entsprechend deinem get_chartdata-Service-Aufruf. Der Sensor aktualisiert automatisch bei Preisdaten-Updates (typischerweise nach Mitternacht und wenn morgige Daten eintreffen). Greife auf die Service-Response-Daten direkt über die Entity-Attribute zu - die Struktur entspricht exakt dem, was get_chartdata zurückgibt."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
@ -464,11 +469,6 @@
|
||||||
"description": "Ob die Echtzeit-Verbrauchsüberwachung aktiv ist",
|
"description": "Ob die Echtzeit-Verbrauchsüberwachung aktiv ist",
|
||||||
"long_description": "Zeigt an, ob die Echtzeit-Stromverbrauchsüberwachung für dein Tibber-Zuhause aktiviert und aktiv ist. Dies erfordert kompatible Messhardware (z. B. Tibber Pulse) und ein aktives Abonnement.",
|
"long_description": "Zeigt an, ob die Echtzeit-Stromverbrauchsüberwachung für dein Tibber-Zuhause aktiviert und aktiv ist. Dies erfordert kompatible Messhardware (z. B. Tibber Pulse) und ein aktives Abonnement.",
|
||||||
"usage_tips": "Verwende dies, um zu überprüfen, ob Echtzeit-Verbrauchsdaten verfügbar sind. Aktiviere Benachrichtigungen, falls dies unerwartet auf 'Aus' wechselt, was auf potenzielle Hardware- oder Verbindungsprobleme hinweist."
|
"usage_tips": "Verwende dies, um zu überprüfen, ob Echtzeit-Verbrauchsdaten verfügbar sind. Aktiviere Benachrichtigungen, falls dies unerwartet auf 'Aus' wechselt, was auf potenzielle Hardware- oder Verbindungsprobleme hinweist."
|
||||||
},
|
|
||||||
"chart_data_export": {
|
|
||||||
"description": "Datenexport für Dashboard-Integrationen",
|
|
||||||
"long_description": "Dieser Binärsensor ruft den get_chartdata-Service auf, um Daten für externe Dashboard-Integrationen wie ApexCharts bereitzustellen. Wird verwendet, um Preisdaten in benutzerdefinierten Visualisierungen anzuzeigen.",
|
|
||||||
"usage_tips": "Konfiguriere die YAML-Parameter in den Integrationsoptionen, um Datenquellen, Zeiträume und Aggregationsmethoden festzulegen. Verwende dies mit benutzerdefinierten Karten oder ApexCharts-Dashboards zur Visualisierung von Preistrends und Verbrauchsdaten."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home_types": {
|
"home_types": {
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,11 @@
|
||||||
"description": "Status for Tibber-abonnementet ditt",
|
"description": "Status for Tibber-abonnementet ditt",
|
||||||
"long_description": "Viser om Tibber-abonnementet ditt for øyeblikket er aktivt, avsluttet eller venter på aktivering. En status 'Aktiv' betyr at du aktivt mottar strøm gjennom Tibber.",
|
"long_description": "Viser om Tibber-abonnementet ditt for øyeblikket er aktivt, avsluttet eller venter på aktivering. En status 'Aktiv' betyr at du aktivt mottar strøm gjennom Tibber.",
|
||||||
"usage_tips": "Bruk dette til å overvåke abonnementsstatusen din. Sett opp varsler hvis statusen endres fra 'Aktiv' for å sikre uavbrutt tjeneste."
|
"usage_tips": "Bruk dette til å overvåke abonnementsstatusen din. Sett opp varsler hvis statusen endres fra 'Aktiv' for å sikre uavbrutt tjeneste."
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"description": "Dataeksport for dashboardintegrasjoner",
|
||||||
|
"long_description": "Denne sensoren kaller get_chartdata-tjenesten med din konfigurerte YAML-konfigurasjon og eksponerer resultatet som entitetsattributter. Status viser 'ready' når data er tilgjengelig, 'error' ved feil, eller 'pending' før første kall. Perfekt for dashboardintegrasjoner som ApexCharts som trenger å lese prisdata fra entitetsattributter.",
|
||||||
|
"usage_tips": "Konfigurer YAML-parametrene i integrasjonsinnstillingene for å matche get_chartdata-tjenestekallet ditt. Sensoren vil automatisk oppdatere når prisdata oppdateres (typisk etter midnatt og når morgendagens data ankommer). Få tilgang til tjenesteresponsdataene direkte fra entitetens attributter - strukturen matcher nøyaktig det get_chartdata returnerer."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
|
||||||
|
|
@ -430,8 +430,13 @@
|
||||||
},
|
},
|
||||||
"subscription_status": {
|
"subscription_status": {
|
||||||
"description": "Status van je Tibber-abonnement",
|
"description": "Status van je Tibber-abonnement",
|
||||||
"long_description": "Toont of je Tibber-abonnement momenteel actief is, is beëindigd of wacht op activering. Een status 'Actief' betekent dat je actief elektriciteit ontvangt via Tibber.",
|
"long_description": "Geeft aan of je Tibber-abonnement momenteel actief is, beëindigd of wacht op activering. Een 'Actief'-status betekent dat je actief elektriciteit via Tibber afneemt.",
|
||||||
"usage_tips": "Gebruik dit om je abonnementsstatus te monitoren. Stel meldingen in als de status verandert van 'Actief' om ononderbroken service te garanderen."
|
"usage_tips": "Gebruik dit om je abonnementsstatus te monitoren. Stel meldingen in als de status verandert van 'Actief' om ononderbroken service te waarborgen."
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"description": "Data-export voor dashboard-integraties",
|
||||||
|
"long_description": "Deze sensor roept de get_chartdata-service aan met jouw geconfigureerde YAML-configuratie en stelt het resultaat beschikbaar als entiteitsattributen. De status toont 'ready' wanneer data beschikbaar is, 'error' bij fouten, of 'pending' voor de eerste aanroep. Perfekt voor dashboard-integraties zoals ApexCharts die prijsgegevens uit entiteitsattributen moeten lezen.",
|
||||||
|
"usage_tips": "Configureer de YAML-parameters in de integratie-opties om overeen te komen met jouw get_chartdata-service-aanroep. De sensor wordt automatisch bijgewerkt wanneer prijsgegevens worden bijgewerkt (typisch na middernacht en wanneer gegevens van morgen binnenkomen). Krijg toegang tot de service-responsgegevens direct vanuit de entiteitsattributen - de structuur komt exact overeen met wat get_chartdata retourneert."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,11 @@
|
||||||
"description": "Status för ditt Tibber-abonnemang",
|
"description": "Status för ditt Tibber-abonnemang",
|
||||||
"long_description": "Visar om ditt Tibber-abonnemang för närvarande är aktivt, har avslutats eller väntar på aktivering. En status 'Aktiv' betyder att du aktivt tar emot elektricitet genom Tibber.",
|
"long_description": "Visar om ditt Tibber-abonnemang för närvarande är aktivt, har avslutats eller väntar på aktivering. En status 'Aktiv' betyder att du aktivt tar emot elektricitet genom Tibber.",
|
||||||
"usage_tips": "Använd detta för att övervaka din abonnemangsstatus. Ställ in varningar om statusen ändras från 'Aktiv' för att säkerställa oavbruten service."
|
"usage_tips": "Använd detta för att övervaka din abonnemangsstatus. Ställ in varningar om statusen ändras från 'Aktiv' för att säkerställa oavbruten service."
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"description": "Dataexport för instrumentpanelsintegrationer",
|
||||||
|
"long_description": "Denna sensor anropar get_chartdata-tjänsten med din konfigurerade YAML-konfiguration och exponerar resultatet som entitetsattribut. Statusen visar 'ready' när data är tillgänglig, 'error' vid fel, eller 'pending' före första anropet. Perfekt för instrumentpanelsintegrationer som ApexCharts som behöver läsa prisdata från entitetsattribut.",
|
||||||
|
"usage_tips": "Konfigurera YAML-parametrarna i integrationsinställningarna för att matcha ditt get_chartdata-tjänsteanrop. Sensorn uppdateras automatiskt när prisdata uppdateras (vanligtvis efter midnatt och när morgondagens data anländer). Få åtkomst till tjänstesvarsdata direkt från entitetens attribut - strukturen matchar exakt vad get_chartdata returnerar."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ from __future__ import annotations
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
from custom_components.tibber_prices.average_utils import (
|
from custom_components.tibber_prices.average_utils import (
|
||||||
calculate_current_leading_avg,
|
calculate_current_leading_avg,
|
||||||
calculate_current_leading_max,
|
calculate_current_leading_max,
|
||||||
|
|
@ -18,6 +20,7 @@ from custom_components.tibber_prices.binary_sensor.attributes import (
|
||||||
get_price_intervals_attributes,
|
get_price_intervals_attributes,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import (
|
||||||
|
CONF_CHART_DATA_CONFIG,
|
||||||
CONF_EXTENDED_DESCRIPTIONS,
|
CONF_EXTENDED_DESCRIPTIONS,
|
||||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
|
@ -33,7 +36,6 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
async_get_entity_description,
|
|
||||||
format_price_unit_major,
|
format_price_unit_major,
|
||||||
format_price_unit_minor,
|
format_price_unit_minor,
|
||||||
get_entity_description,
|
get_entity_description,
|
||||||
|
|
@ -115,6 +117,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
# Centralized trend calculation cache (calculated once per coordinator update)
|
# Centralized trend calculation cache (calculated once per coordinator update)
|
||||||
self._trend_calculation_cache: dict[str, Any] | None = None
|
self._trend_calculation_cache: dict[str, Any] | None = None
|
||||||
self._trend_calculation_timestamp: datetime | None = None
|
self._trend_calculation_timestamp: datetime | None = None
|
||||||
|
# Chart data export (for chart_data_export sensor) - from binary_sensor
|
||||||
|
self._chart_data_last_update = None # Track last service call timestamp
|
||||||
|
self._chart_data_error = None # Track last service call error
|
||||||
|
self._chart_data_response = None # Store service response for attributes
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
|
|
@ -132,6 +138,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._handle_minute_update
|
self._handle_minute_update
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# For chart_data_export, trigger initial service call
|
||||||
|
if self.entity_description.key == "chart_data_export":
|
||||||
|
await self._refresh_chart_data()
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""When entity will be removed from hass."""
|
"""When entity will be removed from hass."""
|
||||||
await super().async_will_remove_from_hass()
|
await super().async_will_remove_from_hass()
|
||||||
|
|
@ -351,6 +361,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
"peak_price_next_in_minutes": lambda: self._get_period_timing_value(
|
"peak_price_next_in_minutes": lambda: self._get_period_timing_value(
|
||||||
period_type="peak_price", value_type="next_in_minutes"
|
period_type="peak_price", value_type="next_in_minutes"
|
||||||
),
|
),
|
||||||
|
# Chart data export sensor
|
||||||
|
"chart_data_export": self._get_chart_data_export_value,
|
||||||
}
|
}
|
||||||
|
|
||||||
return handlers.get(key)
|
return handlers.get(key)
|
||||||
|
|
@ -1967,93 +1979,74 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
# Fall back to static icon from entity description
|
# Fall back to static icon from entity description
|
||||||
return icon or self.entity_description.icon
|
return icon or self.entity_description.icon
|
||||||
|
|
||||||
@property
|
def _get_description_attributes(self) -> dict[str, str]:
|
||||||
async def async_extra_state_attributes(self) -> dict | None:
|
"""Get description/long_description/usage_tips attributes."""
|
||||||
"""Return additional state attributes asynchronously."""
|
attributes = {}
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
attributes = self._get_sensor_attributes() or {}
|
if not self.entity_description.translation_key or not self.hass:
|
||||||
|
return attributes
|
||||||
|
|
||||||
# Add description from the custom translations file
|
|
||||||
if self.entity_description.translation_key and self.hass is not None:
|
|
||||||
# Get user's language preference
|
|
||||||
language = self.hass.config.language if self.hass.config.language else "en"
|
language = self.hass.config.language if self.hass.config.language else "en"
|
||||||
|
|
||||||
# Add basic description
|
# Add basic description (from cache, synchronous)
|
||||||
description = await async_get_entity_description(
|
description = get_entity_description("sensor", self.entity_description.translation_key, language, "description")
|
||||||
self.hass, "sensor", self.entity_description.translation_key, language, "description"
|
|
||||||
)
|
|
||||||
if description:
|
if description:
|
||||||
attributes["description"] = description
|
attributes["description"] = description
|
||||||
|
|
||||||
# Check if extended descriptions are enabled in the config
|
# Check if extended descriptions are enabled
|
||||||
extended_descriptions = self.coordinator.config_entry.options.get(
|
extended_descriptions = self.coordinator.config_entry.options.get(
|
||||||
CONF_EXTENDED_DESCRIPTIONS,
|
CONF_EXTENDED_DESCRIPTIONS,
|
||||||
self.coordinator.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
self.coordinator.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add extended descriptions if enabled
|
|
||||||
if extended_descriptions:
|
if extended_descriptions:
|
||||||
# Add long description if available
|
long_desc = get_entity_description(
|
||||||
long_desc = await async_get_entity_description(
|
"sensor", self.entity_description.translation_key, language, "long_description"
|
||||||
self.hass, "sensor", self.entity_description.translation_key, language, "long_description"
|
|
||||||
)
|
)
|
||||||
if long_desc:
|
if long_desc:
|
||||||
attributes["long_description"] = long_desc
|
attributes["long_description"] = long_desc
|
||||||
|
|
||||||
# Add usage tips if available
|
usage_tips = get_entity_description(
|
||||||
usage_tips = await async_get_entity_description(
|
"sensor", self.entity_description.translation_key, language, "usage_tips"
|
||||||
self.hass, "sensor", self.entity_description.translation_key, language, "usage_tips"
|
|
||||||
)
|
)
|
||||||
if usage_tips:
|
if usage_tips:
|
||||||
attributes["usage_tips"] = usage_tips
|
attributes["usage_tips"] = usage_tips
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||||
|
"""Return additional state attributes."""
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# For chart_data_export: special ordering (metadata → descriptions → service data)
|
||||||
|
if self.entity_description.key == "chart_data_export":
|
||||||
|
attributes: dict[str, Any] = {}
|
||||||
|
chart_attrs = self._get_chart_data_export_attributes()
|
||||||
|
|
||||||
|
# Step 1: Add metadata (timestamp, error)
|
||||||
|
if chart_attrs:
|
||||||
|
for key in ("timestamp", "error"):
|
||||||
|
if key in chart_attrs:
|
||||||
|
attributes[key] = chart_attrs[key]
|
||||||
|
|
||||||
|
# Step 2: Add descriptions
|
||||||
|
description_attrs = self._get_description_attributes()
|
||||||
|
attributes.update(description_attrs)
|
||||||
|
|
||||||
|
# Step 3: Add service data (everything except metadata)
|
||||||
|
if chart_attrs:
|
||||||
|
attributes.update({k: v for k, v in chart_attrs.items() if k not in ("timestamp", "error")})
|
||||||
|
|
||||||
return attributes if attributes else None
|
return attributes if attributes else None
|
||||||
|
|
||||||
@property
|
# For all other sensors: standard behavior
|
||||||
def extra_state_attributes(self) -> dict | None:
|
attributes: dict[str, Any] = self._get_sensor_attributes() or {}
|
||||||
"""
|
description_attrs = self._get_description_attributes()
|
||||||
Return additional state attributes (synchronous version).
|
# Merge description attributes
|
||||||
|
for key, value in description_attrs.items():
|
||||||
This synchronous method is required by Home Assistant and will
|
attributes[key] = value
|
||||||
first return basic attributes, then add cached descriptions
|
|
||||||
without any blocking I/O operations.
|
|
||||||
"""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Start with the basic attributes
|
|
||||||
attributes = self._get_sensor_attributes() or {}
|
|
||||||
|
|
||||||
# Add descriptions from the cache if available (non-blocking)
|
|
||||||
if self.entity_description.translation_key and self.hass is not None:
|
|
||||||
# Get user's language preference
|
|
||||||
language = self.hass.config.language if self.hass.config.language else "en"
|
|
||||||
translation_key = self.entity_description.translation_key
|
|
||||||
|
|
||||||
# Add basic description from cache
|
|
||||||
description = get_entity_description("sensor", translation_key, language, "description")
|
|
||||||
if description:
|
|
||||||
attributes["description"] = description
|
|
||||||
|
|
||||||
# Check if extended descriptions are enabled in the config
|
|
||||||
extended_descriptions = self.coordinator.config_entry.options.get(
|
|
||||||
CONF_EXTENDED_DESCRIPTIONS,
|
|
||||||
self.coordinator.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add extended descriptions if enabled (from cache only)
|
|
||||||
if extended_descriptions:
|
|
||||||
# Add long description if available in cache
|
|
||||||
long_desc = get_entity_description("sensor", translation_key, language, "long_description")
|
|
||||||
if long_desc:
|
|
||||||
attributes["long_description"] = long_desc
|
|
||||||
|
|
||||||
# Add usage tips if available in cache
|
|
||||||
usage_tips = get_entity_description("sensor", translation_key, language, "usage_tips")
|
|
||||||
if usage_tips:
|
|
||||||
attributes["usage_tips"] = usage_tips
|
|
||||||
|
|
||||||
return attributes if attributes else None
|
return attributes if attributes else None
|
||||||
|
|
||||||
|
|
@ -2061,6 +2054,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
"""Get attributes based on sensor type."""
|
"""Get attributes based on sensor type."""
|
||||||
key = self.entity_description.key
|
key = self.entity_description.key
|
||||||
|
|
||||||
|
# Special handling for chart_data_export - returns chart data in attributes
|
||||||
|
if key == "chart_data_export":
|
||||||
|
return self._get_chart_data_export_attributes()
|
||||||
|
|
||||||
# Prepare cached data that attribute builders might need
|
# Prepare cached data that attribute builders might need
|
||||||
cached_data = {
|
cached_data = {
|
||||||
"trend_attributes": getattr(self, "_trend_attributes", None),
|
"trend_attributes": getattr(self, "_trend_attributes", None),
|
||||||
|
|
@ -2094,3 +2091,117 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Force a refresh when homeassistant.update_entity is called."""
|
"""Force a refresh when homeassistant.update_entity is called."""
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
# For chart_data_export, also refresh the service call
|
||||||
|
if self.entity_description.key == "chart_data_export":
|
||||||
|
await self._refresh_chart_data()
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# CHART DATA EXPORT METHODS
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
def _get_chart_data_export_value(self) -> str | None:
|
||||||
|
"""Return state for chart_data_export sensor."""
|
||||||
|
if self._chart_data_error:
|
||||||
|
return "error"
|
||||||
|
if self._chart_data_last_update:
|
||||||
|
return "ready"
|
||||||
|
return "pending"
|
||||||
|
|
||||||
|
async def _refresh_chart_data(self) -> None:
|
||||||
|
"""Refresh chart data by calling get_chartdata service."""
|
||||||
|
await self._call_chartdata_service_async()
|
||||||
|
# Result stored in cache variables, no need to return
|
||||||
|
# Trigger state update after refresh
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def _call_chartdata_service_async(self) -> dict | None:
|
||||||
|
"""Call get_chartdata service with user-configured YAML (async)."""
|
||||||
|
# Get user-configured YAML
|
||||||
|
yaml_config = self.coordinator.config_entry.options.get(CONF_CHART_DATA_CONFIG, "")
|
||||||
|
|
||||||
|
# Parse YAML if provided, otherwise use empty dict (service defaults)
|
||||||
|
service_params = {}
|
||||||
|
if yaml_config and yaml_config.strip():
|
||||||
|
try:
|
||||||
|
parsed = yaml.safe_load(yaml_config)
|
||||||
|
# Ensure we have a dict (yaml.safe_load can return str, int, etc.)
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
service_params = parsed
|
||||||
|
else:
|
||||||
|
self.coordinator.logger.warning(
|
||||||
|
"YAML configuration must be a dictionary, got %s. Using service defaults.",
|
||||||
|
type(parsed).__name__,
|
||||||
|
extra={"entity": self.entity_description.key},
|
||||||
|
)
|
||||||
|
service_params = {}
|
||||||
|
except yaml.YAMLError as err:
|
||||||
|
self.coordinator.logger.warning(
|
||||||
|
"Invalid chart data YAML configuration: %s. Using service defaults.",
|
||||||
|
err,
|
||||||
|
extra={"entity": self.entity_description.key},
|
||||||
|
)
|
||||||
|
service_params = {} # Fall back to service defaults
|
||||||
|
|
||||||
|
# Add required entry_id parameter
|
||||||
|
service_params["entry_id"] = self.coordinator.config_entry.entry_id
|
||||||
|
|
||||||
|
# Call get_chartdata service using official HA service system
|
||||||
|
try:
|
||||||
|
response = await self.hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
"get_chartdata",
|
||||||
|
service_params,
|
||||||
|
blocking=True,
|
||||||
|
return_response=True,
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
self.coordinator.logger.exception(
|
||||||
|
"Chart data service call failed",
|
||||||
|
extra={"entity": self.entity_description.key},
|
||||||
|
)
|
||||||
|
self._chart_data_response = None
|
||||||
|
self._chart_data_last_update = dt_util.now()
|
||||||
|
self._chart_data_error = str(ex)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
self._chart_data_response = response
|
||||||
|
self._chart_data_last_update = dt_util.now()
|
||||||
|
self._chart_data_error = None
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _get_chart_data_export_attributes(self) -> dict[str, object] | None:
|
||||||
|
"""
|
||||||
|
Return chart data from last service call as attributes with metadata.
|
||||||
|
|
||||||
|
Attribute order: timestamp, error (if any), service data (at the end).
|
||||||
|
Note: description/long_description/usage_tips are added BEFORE these attributes
|
||||||
|
by async_extra_state_attributes() / extra_state_attributes().
|
||||||
|
"""
|
||||||
|
# Build base attributes with metadata FIRST
|
||||||
|
attributes: dict[str, object] = {
|
||||||
|
"timestamp": self._chart_data_last_update.isoformat() if self._chart_data_last_update else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add error message if service call failed
|
||||||
|
if self._chart_data_error:
|
||||||
|
attributes["error"] = self._chart_data_error
|
||||||
|
|
||||||
|
if not self._chart_data_response:
|
||||||
|
# No data - only metadata (timestamp, error)
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
# Service data goes LAST - after metadata
|
||||||
|
# Descriptions will be inserted BEFORE this by the property methods
|
||||||
|
if isinstance(self._chart_data_response, dict):
|
||||||
|
if len(self._chart_data_response) > 1:
|
||||||
|
# Multiple keys → wrap to prevent collision with metadata
|
||||||
|
attributes["data"] = self._chart_data_response
|
||||||
|
else:
|
||||||
|
# Single key → safe to merge directly
|
||||||
|
attributes.update(self._chart_data_response)
|
||||||
|
else:
|
||||||
|
# If response is array/list/primitive, wrap it in "data" key
|
||||||
|
attributes["data"] = self._chart_data_response
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
|
||||||
|
|
@ -1000,6 +1000,16 @@ DIAGNOSTIC_SENSORS = (
|
||||||
options=["running", "ended", "pending", "unknown"],
|
options=["running", "ended", "pending", "unknown"],
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="chart_data_export",
|
||||||
|
translation_key="chart_data_export",
|
||||||
|
name="Chart Data Export",
|
||||||
|
icon="mdi:database-export",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=["pending", "ready", "error"],
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False, # Opt-in
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -648,6 +648,14 @@
|
||||||
"pending": "Ausstehend",
|
"pending": "Ausstehend",
|
||||||
"unknown": "Unbekannt"
|
"unknown": "Unbekannt"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"name": "Diagramm-Datenexport",
|
||||||
|
"state": {
|
||||||
|
"pending": "Ausstehend",
|
||||||
|
"ready": "Bereit",
|
||||||
|
"error": "Fehler"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
@ -668,9 +676,6 @@
|
||||||
},
|
},
|
||||||
"realtime_consumption_enabled": {
|
"realtime_consumption_enabled": {
|
||||||
"name": "Echtzeitverbrauch aktiviert"
|
"name": "Echtzeitverbrauch aktiviert"
|
||||||
},
|
|
||||||
"chart_data_export": {
|
|
||||||
"name": "Diagramm-Datenexport"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -644,6 +644,14 @@
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"unknown": "Unknown"
|
"unknown": "Unknown"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"name": "Chart Data Export",
|
||||||
|
"state": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"ready": "Ready",
|
||||||
|
"error": "Error"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
@ -664,9 +672,6 @@
|
||||||
},
|
},
|
||||||
"realtime_consumption_enabled": {
|
"realtime_consumption_enabled": {
|
||||||
"name": "Realtime Consumption Enabled"
|
"name": "Realtime Consumption Enabled"
|
||||||
},
|
|
||||||
"chart_data_export": {
|
|
||||||
"name": "Chart Data Export"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -644,6 +644,14 @@
|
||||||
"pending": "Venter",
|
"pending": "Venter",
|
||||||
"unknown": "Ukjent"
|
"unknown": "Ukjent"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"name": "Diagramdataeksport",
|
||||||
|
"state": {
|
||||||
|
"pending": "Venter",
|
||||||
|
"ready": "Klar",
|
||||||
|
"error": "Feil"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
@ -664,9 +672,6 @@
|
||||||
},
|
},
|
||||||
"realtime_consumption_enabled": {
|
"realtime_consumption_enabled": {
|
||||||
"name": "Sanntidsforbruk aktivert"
|
"name": "Sanntidsforbruk aktivert"
|
||||||
},
|
|
||||||
"chart_data_export": {
|
|
||||||
"name": "Diagramdataeksport"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -644,6 +644,14 @@
|
||||||
"pending": "In afwachting",
|
"pending": "In afwachting",
|
||||||
"unknown": "Onbekend"
|
"unknown": "Onbekend"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"name": "Grafiek Data Export",
|
||||||
|
"state": {
|
||||||
|
"pending": "In afwachting",
|
||||||
|
"ready": "Klaar",
|
||||||
|
"error": "Fout"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
@ -664,9 +672,6 @@
|
||||||
},
|
},
|
||||||
"realtime_consumption_enabled": {
|
"realtime_consumption_enabled": {
|
||||||
"name": "Realtime verbruik ingeschakeld"
|
"name": "Realtime verbruik ingeschakeld"
|
||||||
},
|
|
||||||
"chart_data_export": {
|
|
||||||
"name": "Grafiek Data Export"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -644,6 +644,14 @@
|
||||||
"pending": "Väntar",
|
"pending": "Väntar",
|
||||||
"unknown": "Okänd"
|
"unknown": "Okänd"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"chart_data_export": {
|
||||||
|
"name": "Diagramdataexport",
|
||||||
|
"state": {
|
||||||
|
"pending": "Väntar",
|
||||||
|
"ready": "Redo",
|
||||||
|
"error": "Fel"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
|
|
@ -664,9 +672,6 @@
|
||||||
},
|
},
|
||||||
"realtime_consumption_enabled": {
|
"realtime_consumption_enabled": {
|
||||||
"name": "Realtidsförbrukning aktiverad"
|
"name": "Realtidsförbrukning aktiverad"
|
||||||
},
|
|
||||||
"chart_data_export": {
|
|
||||||
"name": "Diagramdataexport"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ Coming soon...
|
||||||
|
|
||||||
### Chart Data Export
|
### Chart Data Export
|
||||||
|
|
||||||
**Entity ID:** `binary_sensor.tibber_home_NAME_chart_data_export`
|
**Entity ID:** `sensor.tibber_home_NAME_chart_data_export`
|
||||||
**Default State:** Disabled (must be manually enabled)
|
**Default State:** Disabled (must be manually enabled)
|
||||||
|
|
||||||
> **⚠️ Legacy Feature**: This sensor is maintained for backward compatibility. For new integrations, use the **`tibber_prices.get_chartdata`** service instead, which offers more flexibility and better performance.
|
> **⚠️ Legacy Feature**: This sensor is maintained for backward compatibility. For new integrations, use the **`tibber_prices.get_chartdata`** service instead, which offers more flexibility and better performance.
|
||||||
|
|
@ -45,6 +45,7 @@ This diagnostic sensor provides cached chart-friendly price data that can be con
|
||||||
- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7)
|
- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7)
|
||||||
- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes)
|
- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes)
|
||||||
- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access
|
- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access
|
||||||
|
- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed)
|
||||||
|
|
||||||
**Important Notes:**
|
**Important Notes:**
|
||||||
|
|
||||||
|
|
@ -54,8 +55,10 @@ This diagnostic sensor provides cached chart-friendly price data that can be con
|
||||||
|
|
||||||
**Attributes:**
|
**Attributes:**
|
||||||
|
|
||||||
The sensor exposes a single attribute containing the chart data in your configured format:
|
The sensor exposes chart data with metadata in attributes:
|
||||||
|
|
||||||
|
- **`timestamp`**: When the data was last fetched
|
||||||
|
- **`error`**: Error message if service call failed
|
||||||
- **`data`** (or custom name): Array of price data points in configured format
|
- **`data`** (or custom name): Array of price data points in configured format
|
||||||
|
|
||||||
**Configuration:**
|
**Configuration:**
|
||||||
|
|
@ -78,7 +81,7 @@ See the `tibber_prices.get_chartdata` service documentation below for a complete
|
||||||
# ApexCharts card consuming the sensor
|
# ApexCharts card consuming the sensor
|
||||||
type: custom:apexcharts-card
|
type: custom:apexcharts-card
|
||||||
series:
|
series:
|
||||||
- entity: binary_sensor.tibber_home_chart_data_export
|
- entity: sensor.tibber_home_chart_data_export
|
||||||
data_generator: |
|
data_generator: |
|
||||||
return entity.attributes.data;
|
return entity.attributes.data;
|
||||||
```
|
```
|
||||||
|
|
@ -91,7 +94,7 @@ If you're currently using this sensor, consider migrating to the service:
|
||||||
# Old approach (sensor)
|
# Old approach (sensor)
|
||||||
- service: apexcharts_card.update
|
- service: apexcharts_card.update
|
||||||
data:
|
data:
|
||||||
entity: binary_sensor.tibber_home_chart_data_export
|
entity: sensor.tibber_home_chart_data_export
|
||||||
|
|
||||||
# New approach (service)
|
# New approach (service)
|
||||||
- service: tibber_prices.get_chartdata
|
- service: tibber_prices.get_chartdata
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ data:
|
||||||
|
|
||||||
## Migration from Chart Data Export Sensor
|
## Migration from Chart Data Export Sensor
|
||||||
|
|
||||||
If you're currently using the `binary_sensor.tibber_home_chart_data_export` sensor, consider migrating to `tibber_prices.get_chartdata`:
|
If you're currently using the `sensor.tibber_home_chart_data_export` sensor, consider migrating to `tibber_prices.get_chartdata`:
|
||||||
|
|
||||||
**Benefits:**
|
**Benefits:**
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue