hass.tibber_prices/custom_components/tibber_prices/__init__.py
Julian Pawlowski e950737478 feat(chart_export): migrate sensor config from UI to configuration.yaml
Moved Chart Data Export sensor configuration from config flow textarea
to configuration.yaml for better maintainability and consistency with
Home Assistant standards.

Changes:
- __init__.py: Added async_setup() with CONFIG_SCHEMA for tibber_prices.chart_export
- const.py: Added DATA_CHART_CONFIG constant for hass.data storage
- options_flow.py: Simplified chart_data_export step to info-only page
- schemas.py: get_chart_data_export_schema() returns empty schema (no input fields)
- sensor/chart_data.py: Reads config from hass.data instead of config_entry.options
- All 5 translation files: Updated chart_data_export description with:
  - Clear heading: "📊 Chart Data Export Sensor"
  - Intro line explaining sensor purpose
  - Legacy warning (⚠️) recommending service use
  - Two valid use cases (): attribute-only tools, auto-updating data
  - One discouraged use case (): automations should use service directly
  - 3-step activation instructions
  - YAML configuration example with all parameters
  - Correct default behavior: today+tomorrow, 15-minute intervals, prices only

Impact: Users configure chart export in configuration.yaml instead of UI.
Sensor remains disabled by default (diagnostic sensor). Config flow shows
prominent info page guiding users toward service usage while keeping
sensor available for legacy dashboard tools that only read attributes.
2025-11-20 13:41:26 +00:00

192 lines
7 KiB
Python

"""
Custom integration to integrate tibber_prices with Home Assistant.
For more details about this integration, please refer to
https://github.com/jpawlowski/hass.tibber_prices
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.storage import Store
from homeassistant.loader import async_get_loaded_integration
from .api import TibberPricesApiClient
from .const import (
DATA_CHART_CONFIG,
DOMAIN,
LOGGER,
async_load_standard_translations,
async_load_translations,
)
from .coordinator import STORAGE_VERSION, TibberPricesDataUpdateCoordinator
from .data import TibberPricesData
from .services import async_setup_services
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from .data import TibberPricesConfigEntry
PLATFORMS: list[Platform] = [
Platform.SENSOR,
Platform.BINARY_SENSOR,
]
# Configuration schema for configuration.yaml
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional("chart_export"): vol.Schema(
{
vol.Optional("day"): vol.All(vol.Any(str, list), vol.Coerce(list)),
vol.Optional("resolution"): str,
vol.Optional("output_format"): str,
vol.Optional("minor_currency"): bool,
vol.Optional("round_decimals"): vol.All(int, vol.Range(min=0, max=10)),
vol.Optional("include_level"): bool,
vol.Optional("include_rating_level"): bool,
vol.Optional("include_average"): bool,
vol.Optional("level_filter"): vol.All(vol.Any(str, list), vol.Coerce(list)),
vol.Optional("rating_level_filter"): vol.All(vol.Any(str, list), vol.Coerce(list)),
vol.Optional("period_filter"): str,
vol.Optional("insert_nulls"): str,
vol.Optional("add_trailing_null"): bool,
vol.Optional("array_fields"): str,
vol.Optional("start_time_field"): str,
vol.Optional("end_time_field"): str,
vol.Optional("price_field"): str,
vol.Optional("level_field"): str,
vol.Optional("rating_level_field"): str,
vol.Optional("average_field"): str,
vol.Optional("data_key"): str,
}
),
}
),
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the Tibber Prices component from configuration.yaml."""
# Store chart export configuration in hass.data for sensor access
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
# Extract chart_export config if present
domain_config = config.get(DOMAIN, {})
chart_config = domain_config.get("chart_export", {})
if chart_config:
LOGGER.debug("Loaded chart_export configuration from configuration.yaml")
hass.data[DOMAIN][DATA_CHART_CONFIG] = chart_config
else:
LOGGER.debug("No chart_export configuration found in configuration.yaml")
hass.data[DOMAIN][DATA_CHART_CONFIG] = {}
return True
# https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry
async def async_setup_entry(
hass: HomeAssistant,
entry: TibberPricesConfigEntry,
) -> bool:
"""Set up this integration using UI."""
LOGGER.debug(f"[tibber_prices] async_setup_entry called for entry_id={entry.entry_id}")
# Preload translations to populate the cache
await async_load_translations(hass, "en")
await async_load_standard_translations(hass, "en")
# Try to load translations for the user's configured language if not English
if hass.config.language and hass.config.language != "en":
await async_load_translations(hass, hass.config.language)
await async_load_standard_translations(hass, hass.config.language)
# Register services when a config entry is loaded
async_setup_services(hass)
integration = async_get_loaded_integration(hass, entry.domain)
coordinator = TibberPricesDataUpdateCoordinator(
hass=hass,
config_entry=entry,
version=str(integration.version) if integration.version else "unknown",
)
# CRITICAL: Load cache BEFORE first refresh to ensure user_data is available
# for metadata sensors (grid_company, estimated_annual_consumption, etc.)
# This prevents sensors from being marked as "unavailable" on first setup
await coordinator.load_cache()
entry.runtime_data = TibberPricesData(
client=TibberPricesApiClient(
access_token=entry.data[CONF_ACCESS_TOKEN],
session=async_get_clientsession(hass),
version=str(integration.version) if integration.version else "unknown",
),
integration=integration,
coordinator=coordinator,
)
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
if entry.state == ConfigEntryState.SETUP_IN_PROGRESS:
await coordinator.async_config_entry_first_refresh()
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
else:
await coordinator.async_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(
hass: HomeAssistant,
entry: TibberPricesConfigEntry,
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok and entry.runtime_data is not None:
await entry.runtime_data.coordinator.async_shutdown()
# Unregister services if this was the last config entry
if not hass.config_entries.async_entries(DOMAIN):
for service in [
"get_price",
"get_apexcharts_data",
"get_apexcharts_yaml",
"refresh_user_data",
]:
if hass.services.has_service(DOMAIN, service):
hass.services.async_remove(DOMAIN, service)
return unload_ok
async def async_remove_entry(
hass: HomeAssistant,
entry: TibberPricesConfigEntry,
) -> None:
"""Handle removal of an entry."""
if storage := Store(hass, STORAGE_VERSION, f"{DOMAIN}.{entry.entry_id}"):
LOGGER.debug(f"[tibber_prices] async_remove_entry removing cache store for entry_id={entry.entry_id}")
await storage.async_remove()
async def async_reload_entry(
hass: HomeAssistant,
entry: TibberPricesConfigEntry,
) -> None:
"""Reload config entry."""
await hass.config_entries.async_reload(entry.entry_id)