electricity_price_suite is a Home Assistant custom integration for:
- Building and maintaining a price timeline (today/tomorrow) from direct market providers
- Merging source data by strict priority (authoritative source wins)
- Learning consumption profiles from real device runs
- Optimizing device start times against stored timeline data
- Exposing automation-friendly entities and services
The integration keeps one internal timeline store per entry. Provider selection, fallback order, planning, and logger-based optimization all work against that internal timeline rather than against proxy entities.
Each config entry is one of two types:
timelineprofile_logger
Timeline entries manage prices and plans.
Profile logger entries learn reusable device programs from one energy meter.
Each timeline entry creates one timeline instance (for example one meter/tariff/provider context).
Per timeline, the integration exposes:
sensor.<timeline_slug>_pricing_meta(main timeline sensor)sensor.<timeline_slug>_status(high-level status state for automations)sensor.<timeline_slug>_current_pricesensor.<timeline_slug>_current_market_price- Optional consumption/cost sensors when a total-increasing consumption entity is configured:
sensor.<timeline_slug>_consumption_today_kwhsensor.<timeline_slug>_consumption_current_hour_kwhsensor.<timeline_slug>_consumption_month_kwhsensor.<timeline_slug>_consumption_yesterday_kwhsensor.<timeline_slug>_cost_todaysensor.<timeline_slug>_cost_yesterdaysensor.<timeline_slug>_cost_monthsensor.<timeline_slug>_cost_last_monthsensor.<timeline_slug>_cost_today_incl_basic_feesensor.<timeline_slug>_cost_yesterday_incl_basic_feesensor.<timeline_slug>_cost_month_incl_basic_feesensor.<timeline_slug>_cost_last_month_incl_basic_feesensor.<timeline_slug>_avg_paid_price_todaysensor.<timeline_slug>_avg_paid_price_yesterdaysensor.<timeline_slug>_avg_paid_price_monthsensor.<timeline_slug>_avg_paid_price_last_month
- Dynamic plan entities:
sensor.<timeline_slug>_<planner_slug>_<device_slug>
Each profile logger entry exposes:
sensor.<logger_slug>_profile_logger_metasensor.<logger_slug>_profile_<program_key>
The meta sensor represents logger state and active run details.
Each program sensor represents one learned profile and exposes its average total energy and profile metadata.
Providers are ordered by priority:
- Lower numeric value = higher priority
- Priority
0is typically the authoritative source - Merge policy per slot start time:
- Better priority replaces worse priority
- Same priority replaces old value (refresh behavior)
- Worse priority is ignored
Timeline data updates on explicit calls (refresh_timeline, inject_slots) and optional scheduled checks implemented by the integration runtime. Provider fallback behavior is transparent via response logs and sensor attributes.
The optimizer never needs price slots in its payload. It reads already stored timeline data and computes the best candidate start.
When a logger profile is used, the optimizer reads it directly from the internal logger runtime via profile_logger_entity + program_key. No external service hop is required.
- Direct provider timeline refresh for:
TibberSMARDEnergy-ChartsENTSO-E
- Ordered provider fallback chain per timeline
15 -> 60slot aggregation where needed- Never
60 -> 15slot expansion - Priority-based slot merge with replace/ignore logic
- Weighted timeline metrics (including mixed slot durations)
- Current price sensor (always enabled)
- Separate current market price sensor (raw provider market price before EPS surcharges/tax)
- Optional consumption/cost tracking from one total-increasing energy entity
- Status sensor with fixed machine-readable states
- Device plan entity lifecycle: one persistent plan entity per device per planner within a timeline
- Consumption profile logger entries with per-program sensors
- Fine-grained optimizer (profile slot can be smaller than billing slot)
- Direct internal profile loading from suite logger entries
- Separate plan management service for reset/delete lifecycle actions
- Shared suite helpers for datetime parsing/formatting, profile resampling, logger program-key normalization, and input validation
The integration no longer creates logger error notifications on its own.
Instead, it exposes the relevant machine-readable state for Home Assistant automations:
- logger state via
sensor.<logger_slug>_profile_logger_meta - logger reason via the
reasonattribute - plan status and
reasonviasensor.<timeline_slug>_<planner_slug>_<device_slug>
This keeps notification policy outside the integration:
- choose your own language
- choose your own channels
- decide which error cases should notify and which should stay silent
Generic automation examples are available in:
examples/logger_error_notification.yamlexamples/plan_no_candidate_notification.yaml
The repository includes small generic examples that you can adapt to your own setup:
examples/logger_error_notification.yaml- watches a logger meta sensor
- reads
reason,active_program, andstarted_at - shows how to build your own notification policy
examples/plan_no_candidate_notification.yaml- watches a plan entity
- reacts to
status=no-candidate - reports
reason,timeline_entity, andrequested_latest_start
Replace the placeholder entity IDs and notify services in those examples with your own Home Assistant entities.
The integration keeps the external feature set stable, but the runtime internals are split by responsibility:
runtime.py- timeline orchestration, service-facing runtime behavior, scheduling, and entity lifecycle
logger_runtime.py- profile logger orchestration, sampling, profile persistence, and logger-specific services
timeline_stats.py- timeline state building, weighted metrics, current-price detection, and high-level status evaluation
plan_manager.py- plan payload creation, reset handling, profile loading, and plan re-optimization helpers
resolvers.py- target-to-runtime and target-to-plan resolution helpers
time_utils.py- shared timezone-aware ISO parsing and formatting helpers
profile_utils.py- shared profile export normalization and slot resampling helpers
logger_utils.py- shared
program_keynormalization and display-name helpers
- shared
validation.py- shared validation helpers for logger config input
This split was introduced to reduce duplication in the original monolithic runtime and make future changes easier to validate.
- Copy this integration into your Home Assistant config:
custom_components/electricity_price_suite
- Restart Home Assistant.
- Add integration in UI:
- Settings -> Devices & Services -> Add Integration -> Electricity Price Suite
The config flow starts with an entry-type selection:
Price TimelineConsumption Profile Logger
- Timeline core:
- Timeline name
- Billing resolution (
15or60minutes) - Cache retention days
- Price rounding decimals
- Provider chain:
- Number of providers (
1to4) - Ordered provider selection
- Provider-specific settings per step
- Number of providers (
- Consumption and cost tracking:
- Optional total-increasing consumption energy entity
- Percentage surcharge on market price
- Absolute surcharge per kWh
- Energy tax / VAT percent
- Optional simplified basic-fee settings
- Optional flag whether average paid price should include the configured basic fee
- Planner devices:
- One or more planner device names for this timeline
Provider chain notes:
- The integration is EUR-native for now.
15 -> 60aggregation is allowed for providers that only expose 15-minute slots.60 -> 15expansion is never performed.- Tibber defaults to the first home and only asks for
home_indexwhen multiple homes are enabled.
- Logger name
- Total-increasing energy entity
- Slot minutes
- Maximum allowed power delta
- Auto-create programs on unknown runs
- Optional allowed/block lists for program keys
Main timeline sensor with:
- State: average price today (rounded) or
unknown - Attributes: timeline metrics, day rows, source/fetch metadata, merge-relevant info, and the configured energy price formula values
Automation-friendly status state:
no_datatoday_onlytomorrow_onlytomorrow_not_from_provider_1today_and_tomorrow
Includes attributes like today_rows, tomorrow_rows, and last_source_chain_fetch_at.
- State: current slot price (rounded)
- Minimal attributes for current price context
- State: current raw market price before EPS energy surcharges and tax
- Minimal attributes for current market price context
If a timeline is configured with a total-increasing consumption energy entity, the integration exposes dedicated consumption/cost sensors.
- Consumption sensors expose
kWh - Cost sensors expose
EUR - Average paid price sensors expose
EUR/kWh - The consumption path is re-sampled every 30 seconds, so
current hourand running averages are near-live without keeping 30-second raw rows - Recorder will store their history like normal Home Assistant sensors
last_monthvalues are preserved via monthly rollups and do not require retention beyond 31 days
Basic fee modes:
nonemonthly- Internally prorated to a daily share (
monthly_fee / days_in_month)
- Internally prorated to a daily share (
daily- Treated as a fixed fee per elapsed day
For month-level incl_basic_fee sensors, the fee is shown as the current accumulated month-to-date share.
Average paid price sensors can optionally include the configured basic fee through the timeline option avg_price_include_basic_fee.
Per-device planning entity:
- State: planned start timestamp (or
unknown) - Attributes: optimization window, duration, profile details, cost result, run metadata
- Variant-related attributes include:
program_key_usedprogram_display_name_used
Logger meta sensor with:
- State:
idle | running | error - Attributes: active run details, known profiles, last error, sampling metadata
Per-program learned profile sensor with:
- State: average total energy in kWh
- Attributes:
program_key,program_name,run_count,slot_minutes,slot_count,runtime_minutes,last_updated
All services are in domain electricity_price_suite.
refresh_timeline,inject_slots,optimize_deviceuse a timeline target.manage_planuses one or more plan entity targets.manage_profile_run,manage_profileuse a profile logger target.
Refreshes timeline slots from configured sources and merges them by priority.
target(required): timeline entity target (sensor.<timeline_slug>_pricing_meta).- Expected: exactly one sensor entity in
target.entity_id. - Effect: selects which timeline instance is refreshed.
- Expected: exactly one sensor entity in
sources(optional): temporary source override for this call.- Expected: list of source objects with the same shape as stored pull sources.
- Effect: only this refresh call uses these sources; stored source chain is unchanged.
overwrite(optional, defaultfalse): explicit fresh re-fetch mode.- Expected: boolean.
- Effect: deletes currently stored rows for today and tomorrow before fetching again from the source chain.
status:ok | no_datatimeline_entity: resolved timeline entity id.timeline_status: high-level timeline status (no_data,today_only, ...).used_source: first source that produced usable data in this run.used_sources: all sources that contributed rows in this run.attempt_log: list of attempts (source_id,source_type,success,rows,reason).rows_today: number of stored rows for today after merge.rows_tomorrow: number of stored rows for tomorrow after merge.has_primary_data_for_tomorrow: whether tomorrow is currently covered by priority-0 rows.pending_primary: whether fallback rows still exist where primary is expected.merge_debug: counters (inserted,replaced,ignored) for this run.cleared_rows: number of today/tomorrow rows removed before fetch whenoverwrite=true.last_source_chain_fetch_at: timestamp of latest source-chain fetch.
Directly injects slots into timeline storage.
target(required).- Expected: exactly one timeline target entity.
- Effect: chooses which timeline store gets injected data.
slots(required): list of slot objects.- Expected per item:
start_time(ISO datetime with timezone),price_per_kwh(number). - Effect: slots are normalized and merged by priority rules.
- Expected per item:
source_name(optional, defaultmanual_inject).- Expected: string identifier.
- Effect: stored as slot source id for traceability.
source_priority(optional, default9999).- Expected: integer, lower = stronger source.
- Effect: controls whether injected rows replace existing rows.
is_primary(optional, defaultfalse).- Expected: boolean.
- Effect: marks injected rows as primary-source rows.
overwrite(optional, defaultfalse).- Expected: boolean.
- Effect: deletes stored rows for the same local dates before injecting the new rows.
status:ok | no_datatimeline_entity: resolved timeline entity id.rows_received: number of normalized rows accepted from payload.merge_debug: counters (inserted,replaced,ignored).pending_primary: whether fallback rows remain in active window.cleared_rows: number of stored rows removed before injection whenoverwrite=true.
Computes best start for one device using timeline data.
target(required).- Expected: exactly one timeline target entity.
- Effect: optimization uses that timeline's stored slots.
planner_name(required).- Expected: name of a configured planner device for that timeline, for example
Geräteplanung. - Effect: selects which planner device should own the resulting plan entity.
- Expected: name of a configured planner device for that timeline, for example
device_name(required).- Expected: string.
- Effect: identifies the per-planner plan entity.
duration_minutes(optional unless profile source provides duration).- Expected: positive number.
- Effect: runtime length used for cost window.
energy_profile(optional).- Expected: numeric list of weights/energy segments.
- Effect: weighted optimization profile; if shorter/longer than required it is normalized internally.
profile_slot_minutes(optional).- Expected: positive integer.
- Effect: slot resolution of
energy_profile; also candidate grid base when not aligned to billing.
billing_slot_minutes(optional).- Expected: positive integer.
- Effect: override billing price raster; by default detected from timeline slots.
profile_logger_entity(optional).- Expected: entity id of a suite profile logger meta sensor.
- Effect: loads a profile directly from the internal logger runtime.
program_key(required whenprofile_logger_entityis used).- Expected: stable program key, for example
auto_2. - Effect: chooses which learned logger profile is used for optimization.
- Expected: stable program key, for example
program_display_name(optional).- Expected: user-facing compact display label, for example
Auto 2 [I,D,S]. - Effect: persists a readable variant label on the plan without changing the technical
program_key.
- Expected: user-facing compact display label, for example
align_start_to_billing_slot(optional, defaultfalse).- Expected: boolean.
- Effect: candidate starts are forced to billing boundaries.
max_extra_cost_percent(optional, default1).- Expected: float >= 0.
- Effect: maximum additional cost in percent that is still acceptable when
prefer_earliest=true.
prefer_earliest(optional, defaulttrue).- Expected: boolean.
- Effect: pick the earliest candidate within the allowed extra-cost threshold instead of the strict absolute minimum.
start_mode(optional, defaultnow).- Expected:
now | in. - Effect: defines start anchor (
nowornow + start_in_minutes).
- Expected:
start_in_minutes(optional, default0).- Expected: number >= 0.
- Effect: used only for
start_mode=in.
deadline_mode(optional, defaultnone).- Expected:
none | start_within | finish_within. - Effect: applies relative deadline constraint.
- Expected:
deadline_minutes(optional).- Expected: number >= 0.
- Effect: relative limit for selected
deadline_mode.
latest_start(optional).- Expected: ISO datetime string.
- Effect: expert override for absolute latest allowed start. Internally normalized to the optimizer's
latest_startboundary.
latest_finish(optional).- Expected: ISO datetime string.
- Effect: expert override for absolute latest allowed finish. Internally converted to a derived
latest_start.
status:ok | no-candidateplan_entity_id: per-device per-planner plan entity id.best_start: planned start datetime (ISO) ornull.best_end: planned finish datetime (ISO) ornull.best_cost: computed optimization cost ornull.reason: explanatory reason forno-candidate.requested_latest_start: the originally requested latest-start boundary before any truncation by missing price data.
When profile_logger_entity + program_key is used, optimization tries sources in this order:
- learned profile from the selected logger
- estimated runtime configured on that logger for the same
program_key
If neither exists, the optimizer returns no-candidate with a specific reason.
no_valid_slots_after_parse: no usable price slots were available after parsing.no_duration_or_profile: neither duration nor usable energy profile was provided.invalid_energy_profile: the supplied profile could not be parsed as numbers.invalid_duration_minutes: duration was missing, zero, negative, or not finite.invalid_deadline_minutes: deadline offset was negative or not finite.invalid_latest_start:latest_startwas provided but not parseable as ISO datetime.invalid_latest_finish:latest_finishwas provided but not parseable as ISO datetime.invalid_max_extra_cost_percent: extra-cost threshold was negative or not finite.window_too_short_for_duration: the allowed search window is shorter than the runtime.all_candidates_in_past: all candidate starts fell at or before the current time.incomplete_price_coverage_for_candidates: price data did not fully cover any candidate run.candidates_blocked_by_time_and_price_coverage: some candidates were already in the past and the remaining ones had incomplete price coverage.no_candidate_after_constraints: constraints left no valid candidate, but no more specific optimizer reason applied.
Resets, deletes, or re-optimizes existing plan entities.
target(required).- Expected: one or more existing plan entities (
sensor.<timeline_slug>_<planner_slug>_<device_slug>). - Effect: selected plan entities are managed.
- Expected: one or more existing plan entities (
mode(required).- Expected:
reset | delete | reoptimize. - Effect: chooses which plan-management action is executed.
- Expected:
results: list of per-target results:status:reset | deleted | ok | no-candidate | not_found | not_reoptimizedplan_entity_idreason
Starts, finishes, or aborts a logger run for the selected profile logger.
target(required).- Expected: one profile logger meta sensor or one program profile sensor.
mode(required).- Expected:
start | finish | abort. - Effect: chooses the run-lifecycle action.
- Expected:
program_key(optional when target is already a profile sensor).- Expected: program key string.
- Effect: program to start, finish, or guard during abort.
program_display_name(optional).- Only used for mode=
start. - Expected: human-readable display name such as
Auto 2 [I,D]. - Effect: stored as the profile name when a new profile is created or an existing profile name should be refreshed.
- Only used for mode=
reason(optional).- Only used for mode=
abort. - Expected: one of
manual_abort,program_mismatch,restart_recovery,sampling_delay_exceeded.
- Only used for mode=
Returns, resets, deletes, or manages fallback estimated runtimes for a profile logger.
target(required).mode(required).- Expected:
get | reset | delete | add_estimated_runtimes | list_estimated_runtimes | delete_estimated_runtime | clear_estimated_runtimes. - Effect: chooses which profile-management action is executed.
- Expected:
program_key(optional).- Only used for mode=
get,reset,delete,list_estimated_runtimes,delete_estimated_runtime. - If omitted for mode=
geton the meta sensor, the response returns the known program list.
- Only used for mode=
desired_slot_minutes(optional).- Only used for mode=
get. - Resamples the profile when the requested slot length is an integer multiple or divisor of the stored slot length.
- Only used for mode=
debug(optional).- Only used for mode=
get.
- Only used for mode=
items(optional).- Only used for mode=
add_estimated_runtimes. - Expected: mapping of
program_key -> duration_minutes.
- Only used for mode=
action: electricity_price_suite.manage_profile
target:
entity_id: sensor.dishwasher_profile_logger_meta
data:
mode: add_estimated_runtimes
items:
auto_2: 180
auto_2_i_d_s: 140- mode=
get- profile list or one profile payload
- mode=
resetstatus:ok | not_foundprogram_key
- mode=
deletestatus:ok | not_foundprogram_key
- mode=
add_estimated_runtimesstatus:okcountestimated_runtimes
- mode=
list_estimated_runtimes- without
program_key:ok:truecountestimated_runtimes
- with
program_key:ok:true | falseprogram_keyestimated_runtime_minutesreason:estimated_runtime_not_foundwhen missing
- without
- mode=
delete_estimated_runtimestatus:ok | not_foundprogram_key
- mode=
clear_estimated_runtimesstatus:okcount
- Billing slot and profile slot can differ
- Candidate start grid:
- profile slot grid by default
- billing slot grid if
align_start_to_billing_slot=true
- Costs are overlap-weighted across price segments
- Deadlines can be constrained by currently available price coverage
- If a previous plan was data-truncated and new price coverage arrives before planned start, the integration can re-optimize and update the plan
- Timeline slots are stored in integration-managed storage per timeline entry
- Provider metadata and plan payloads are persisted
- Cache retention controls historical cleanup behavior
This integration includes local brand assets:
custom_components/electricity_price_suite/brand/icon.pngcustom_components/electricity_price_suite/brand/logo.png
Repository includes unit tests in tests/ for key logic:
- slot normalization
- priority merge behavior
- optimizer candidate behavior and edge cases
These tests are recommended to keep, because they protect core algorithm behavior during refactors.
- Requires Home Assistant with support for this integration version (
manifest.json) - Use Home Assistant service developer tools to test provider and optimizer flows
- For production usage, configure at least one reliable priority-0 provider
Thanks to the Home Assistant ecosystem and maintainers of related integrations that make flexible price workflows possible, especially:
- EPEX Spot for Home Assistant
- The official Home Assistant Tibber integration