feat(blueprints): add appliance scheduling blueprints with auto-install

Bundle automation and script blueprints into the integration and
install them automatically at HA startup via _install_blueprints().
Remove them cleanly when the last config entry is removed.

Automation blueprints (standalone):
- dishwasher, washing_machine, dryer — smart plug and Home Connect
  variants (HC: door/remote-start sensors; HC Alt: program selector)

Automation blueprints (pipeline):
- laundry_day_pipeline — chains washer → dryer for multiple loads,
  HC and HC Alt variants

Other automation blueprints:
- ev_charging, heat_pump_price_level, heat_pump_smart_boost,
  home_battery, water_heater, laundry_day_pipeline (smart plug)

Script blueprint:
- notify_residents — presence-aware dispatcher for up to 10 residents
  with auto-discovered mobile_app notify services, iOS/Android push
  settings, and per-resident notify overrides

Notification UX across all blueprints:
- Apple Watch-optimised titles (~25 chars) and messages (most
  important info first, middle-dot separators, emoji anchors)
- Customisable notification titles via blueprint inputs (standalone)
- Comma-separated notify services for simple multi-target delivery
- Advanced script path for presence filtering and platform push data

Impact: Users get ready-to-use blueprints installed automatically
with the integration for scheduling appliances during cheap Tibber
price windows. No manual import required.
This commit is contained in:
Julian Pawlowski 2026-04-20 18:45:05 +00:00
parent 2d2873f75f
commit e75e0ed1dc
19 changed files with 8627 additions and 0 deletions

View file

@ -7,6 +7,8 @@ https://github.com/jpawlowski/hass.tibber_prices
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import shutil
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import voluptuous as vol import voluptuous as vol
@ -93,6 +95,53 @@ CONFIG_SCHEMA = vol.Schema(
) )
def _install_blueprints(config_dir: str) -> None:
"""Copy bundled blueprints to the HA config blueprints directory.
Always overwrites existing files so blueprints stay in sync with the
integration version. Removes orphan files that are no longer shipped.
Handles both automation and script blueprint domains.
"""
for bp_domain in ("automation", "script"):
src = Path(__file__).parent / "blueprints" / bp_domain
dst = Path(config_dir) / "blueprints" / bp_domain / DOMAIN
if not src.is_dir():
LOGGER.debug("No bundled %s blueprints directory found, skipping", bp_domain)
continue
dst.mkdir(parents=True, exist_ok=True)
shipped: set[str] = set()
for src_file in src.rglob("*.yaml"):
rel = src_file.relative_to(src)
# Only copy files from the tibber_prices sub-folder
if rel.parts[0] != DOMAIN:
continue
dest_file = Path(config_dir) / "blueprints" / bp_domain / rel
dest_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_file, dest_file)
shipped.add(rel.parts[-1])
# Remove orphan blueprints no longer shipped with the integration
if dst.is_dir():
for existing in dst.glob("*.yaml"):
if existing.name not in shipped:
existing.unlink()
LOGGER.info("Removed orphan %s blueprint %s", bp_domain, existing.name)
LOGGER.debug("Installed %d bundled %s blueprints to %s", len(shipped), bp_domain, dst)
def _remove_blueprints(config_dir: str) -> None:
"""Remove all integration-managed blueprints from the config directory."""
for bp_domain in ("automation", "script"):
bp_dir = Path(config_dir) / "blueprints" / bp_domain / DOMAIN
if bp_dir.is_dir():
shutil.rmtree(bp_dir)
LOGGER.info("Removed bundled %s blueprints directory %s", bp_domain, bp_dir)
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the Tibber Prices component from configuration.yaml.""" """Set up the Tibber Prices component from configuration.yaml."""
# Store chart export configuration in hass.data for sensor access # Store chart export configuration in hass.data for sensor access
@ -120,6 +169,9 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
LOGGER.debug("No chart_metadata configuration found in configuration.yaml") LOGGER.debug("No chart_metadata configuration found in configuration.yaml")
hass.data[DOMAIN][DATA_CHART_METADATA_CONFIG] = {} hass.data[DOMAIN][DATA_CHART_METADATA_CONFIG] = {}
# Install/update bundled blueprints
await hass.async_add_executor_job(_install_blueprints, hass.config.config_dir)
return True return True
@ -366,6 +418,11 @@ async def async_remove_entry(
await async_remove_pool_storage(hass, entry.entry_id) await async_remove_pool_storage(hass, entry.entry_id)
LOGGER.debug(f"[tibber_prices] async_remove_entry removed interval pool storage for entry_id={entry.entry_id}") LOGGER.debug(f"[tibber_prices] async_remove_entry removed interval pool storage for entry_id={entry.entry_id}")
# Remove bundled blueprints if this was the last config entry
remaining = [e for e in hass.config_entries.async_entries(DOMAIN) if e.entry_id != entry.entry_id]
if not remaining:
await hass.async_add_executor_job(_remove_blueprints, hass.config.config_dir)
async def async_reload_entry( async def async_reload_entry(
hass: HomeAssistant, hass: HomeAssistant,

View file

@ -0,0 +1,295 @@
blueprint:
name: "Tibber Prices: Dishwasher (Smart Plug)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Automatically run your dishwasher at the cheapest electricity price
overnight using a smart plug.
Open your
[Tibber Prices configuration](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)
to verify the integration is installed and set up.
**What it does:**
- Plans the cheapest 2-hour window overnight (every evening)
- Starts the dishwasher automatically at the cheapest time
- Sends a notification with the planned time and price
- Survives Home Assistant restarts (uses `input_datetime` helper)
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- One helper (created in Settings → Helpers):
- Date & Time (`input_datetime`) — stores the planned start time
- Smart plug switch for the dishwasher
**How it works:**
1. Every evening at the configured time, the blueprint finds the
cheapest window overnight
2. The planned start time is saved to the helper (survives restarts)
3. At the planned time, the smart plug turns on
4. A notification confirms the plan and the start
**Other variants:**
[Home Connect](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher_home_connect.yaml)
·
[Home Connect Alt](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher_home_connect_alt.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher.yaml
input:
appliance:
name: Appliance
icon: mdi:dishwasher
description: Select the smart plug that controls your dishwasher.
input:
appliance_switch:
name: Dishwasher Smart Plug
description: The switch entity controlling the dishwasher.
selector:
entity:
filter:
domain: switch
schedule:
name: Schedule
icon: mdi:calendar-clock
description: Configure when to plan and the search window.
input:
plan_time:
name: Planning Time
description: >
When to search for the cheapest window each day.
Typically in the evening after loading the dishwasher.
default: "20:00:00"
selector:
time:
start_helper:
name: Start Time Helper
description: >
An `input_datetime` helper (type: Date and Time) that stores
the planned start time. Create in Settings → Helpers.
selector:
entity:
filter:
domain: input_datetime
duration:
name: Program Duration
description: >
Typical dishwasher program duration in minutes.
ECO 50°C ≈ 120 min, Auto ≈ 90 min, Intensive ≈ 150 min.
default: 120
selector:
number:
min: 30
max: 240
step: 5
unit_of_measurement: min
mode: slider
search_start:
name: Search Window Start
description: >
Earliest time the dishwasher may start.
Typically late evening after loading.
default: "22:00:00"
selector:
time:
search_end:
name: Search Window End
description: >
Latest time the dishwasher must finish by.
The program must complete before this time.
default: "06:00:00"
selector:
time:
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect helpers to override settings from your
dashboard at runtime. When a helper is connected and has
a valid value, it takes priority over the fixed default.
Leave empty to always use the fixed defaults.
input:
duration_override:
name: "Override: Program Duration"
description: >
`input_number` helper to change the duration from your
dashboard without reconfiguring the blueprint.
**Create in Settings → Helpers → Number** with the same
min/max as the Duration slider above.
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: Optional mobile notifications for planning and start.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: time
at: !input plan_time
id: plan
- trigger: time
at: !input start_helper
id: execute
variables:
_blueprint_variant: "smart_plug"
appliance_switch: !input appliance_switch
start_helper: !input start_helper
_duration_default: !input duration
_duration_override: !input duration_override
duration: >
{% set o = _duration_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | int(_duration_default) }}
{% else %}
{{ _duration_default }}
{% endif %}
search_start: !input search_start
search_end: !input search_end
notify_service: !input notify_service
actions:
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🍽️ Dishwasher — Setup Required"
message: >
The Tibber Prices integration is not installed or not
configured. Install it via HACS and set up your Tibber
account before using this blueprint.
- stop: "Tibber Prices integration not found"
# ════════════════════════════════════════════════════════
# PLAN: Find cheapest window
# ════════════════════════════════════════════════════════
- choose:
- conditions:
- condition: trigger
id: plan
sequence:
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
search_start_time: "{{ search_start }}"
search_end_time: "{{ search_end }}"
search_end_day_offset: 1
response_variable: result
- if:
- condition: template
value_template: "{{ result.window_found }}"
then:
- action: input_datetime.set_datetime
target:
entity_id: "{{ start_helper }}"
data:
datetime: "{{ result.window.start }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🍽️ Dishwasher Planned"
message: >
Start at {{ result.window.start | as_datetime
| as_local | as_timestamp
| timestamp_custom('%H:%M') }}.
Avg price: {{ result.window.price_mean | round(1) }}
{{ result.price_unit }}/kWh.
{% if result.relaxation_applied | default(false) %}
(Filters relaxed to find window.)
{% endif %}
else:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🍽️ Dishwasher"
message: >
No cheap window found overnight. Consider running
manually or adjusting the search window.
# ════════════════════════════════════════════════════
# EXECUTE: Start dishwasher
# ════════════════════════════════════════════════════
- conditions:
- condition: trigger
id: execute
sequence:
- action: switch.turn_on
target:
entity_id: "{{ appliance_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🍽️ Dishwasher Started"
message: >
Smart plug turned on. Program should finish in
~{{ duration }} minutes.

View file

@ -0,0 +1,445 @@
blueprint:
name: "Tibber Prices: Dishwasher (Home Connect)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v2.0.0
**Device-driven** dishwasher automation with electricity price
optimization using the **Home Connect** integration (HA Core).
**How it works:**
1. Select your program on the dishwasher
2. Close the door and enable Remote Start
3. The blueprint reads the estimated duration from the device
4. Finds the cheapest electricity window before your deadline
5. Tells the dishwasher when to start via `StartInRelative`
6. The dishwasher manages the countdown internally — no HA timers
**No scheduling needed** — the dishwasher handles the delayed start
itself. No `input_datetime` helpers required. Survives HA restarts
because the countdown runs on the appliance.
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- [Home Connect](https://www.home-assistant.io/integrations/home_connect/) integration configured
- **Remote Start** enabled on the dishwasher
**Other variants:**
[Smart Plug](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher.yaml)
·
[Home Connect Alt](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher_home_connect_alt.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.11.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher_home_connect.yaml
input:
appliance:
name: Appliance
icon: mdi:dishwasher
description: >
Select your Home Connect dishwasher device and entities.
input:
appliance_device:
name: Dishwasher Device
description: >
Your dishwasher from the Home Connect integration.
Used to target the start command.
selector:
device:
filter:
integration: home_connect
door_sensor:
name: Door Sensor
description: >
The door sensor of your dishwasher
(e.g., `binary_sensor.dishwasher_door`).
selector:
entity:
filter:
integration: home_connect
domain: binary_sensor
device_class: door
remote_start_sensor:
name: Remote Control Sensor
description: >
The "Remote Control Active" binary sensor
(e.g., `binary_sensor.dishwasher_remote_control`).
Must be **on** for the automation to proceed.
selector:
entity:
filter:
integration: home_connect
domain: binary_sensor
estimated_duration_entity:
name: Estimated Program Duration
description: >
The "Estimated Total Program Time" sensor.
If unavailable, the fallback duration is used instead.
selector:
entity:
filter:
integration: home_connect
domain: sensor
operation_state_entity:
name: Operation State
description: >
The "Operation State" sensor.
Used to verify the machine is ready before planning.
selector:
entity:
filter:
integration: home_connect
domain: sensor
schedule:
name: Schedule
icon: mdi:calendar-clock
description: >
Configure the deadline and fallback duration.
input:
must_finish_by:
name: Must Finish By
description: >
The program must be finished by this time.
If this time has already passed today, the deadline
automatically moves to tomorrow (overnight mode).
default: "06:00:00"
selector:
time:
duration_fallback:
name: Fallback Duration (minutes)
description: >
Used **only** if the device doesn't report the estimated
duration. Normally the duration is read automatically.
ECO 50°C ≈ 120 min, Auto ≈ 90 min, Intensive ≈ 150 min.
default: 120
selector:
number:
min: 30
max: 240
step: 5
unit_of_measurement: min
mode: slider
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional notifications. Use **simple mode** (just a service)
or point to an **advanced script** for multi-target,
presence-aware, and platform-specific notifications.
input:
notify_service:
name: Quick Notification (Simple)
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Ignored when the advanced script is set.
default: ""
selector:
text:
notification_script:
name: Notification Script (Advanced)
description: >
A `script.*` entity for advanced notifications
(multiple recipients, presence filtering, iOS/Android).
When set, replaces the simple notification.
Receives structured variables (event_type, appliance,
title, message, and context data).
default: ""
selector:
entity:
filter:
domain: script
title_setup_required:
name: "Title: Setup Required"
default: "🍽️ Dishwasher — Setup Required"
selector:
text:
title_not_ready:
name: "Title: Not Ready"
default: "🍽️ Dishwasher — Not Ready"
selector:
text:
title_no_cheap_slot:
name: "Title: No Cheap Slot"
default: "🍽️ Dishwasher — No Cheap Slot"
selector:
text:
title_planned:
name: "Title: Planned"
default: "🍽️ Dishwasher — Planned!"
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: state
entity_id: !input door_sensor
to: "off"
- trigger: state
entity_id: !input remote_start_sensor
to: "on"
conditions:
- condition: state
entity_id: !input door_sensor
state: "off"
- condition: state
entity_id: !input remote_start_sensor
state: "on"
variables:
_blueprint_variant: "home_connect"
appliance_device: !input appliance_device
door_sensor: !input door_sensor
remote_start_sensor: !input remote_start_sensor
estimated_duration_entity: !input estimated_duration_entity
operation_state_entity: !input operation_state_entity
must_finish_by_time: !input must_finish_by
duration_fallback: !input duration_fallback
notify_service: !input notify_service
notification_script: !input notification_script
title_setup_required: !input title_setup_required
title_not_ready: !input title_not_ready
title_no_cheap_slot: !input title_no_cheap_slot
title_planned: !input title_planned
actions:
# ════════════════════════════════════════════════════════
# PREFLIGHT CHECKS
# ════════════════════════════════════════════════════════
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- variables:
_n_title: "{{ title_setup_required }}"
_n_message: >
Install the Tibber Prices integration via HACS and
configure your Tibber account.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: setup_required
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Tibber Prices integration not found"
- if:
- condition: template
value_template: >
{% set op = states(operation_state_entity) %}
{{ op not in ['unknown', 'unavailable']
and 'Ready' not in op
and 'Inactive' not in op }}
then:
- variables:
_n_title: "{{ title_not_ready }}"
_n_message: >
State: {{ states(operation_state_entity) }}.
Ensure it's idle with Remote Start enabled.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: not_ready
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Machine not ready"
# ════════════════════════════════════════════════════════
# READ DEVICE DATA
# ════════════════════════════════════════════════════════
- variables:
_raw_duration: "{{ states(estimated_duration_entity) }}"
duration: >
{% set raw = states(estimated_duration_entity) %}
{% if raw not in ['unknown', 'unavailable', 'None', '']
and ':' in raw %}
{% set parts = raw.split(':') %}
{{ (parts[0] | int(0)) * 60 + (parts[1] | int(0)) }}
{% elif raw not in ['unknown', 'unavailable', 'None', '']
and raw | int(0) > 0 %}
{{ raw | int }}
{% else %}
{{ duration_fallback }}
{% endif %}
deadline: >
{% set dl = today_at(must_finish_by_time) %}
{% if dl <= now() %}
{{ (dl + timedelta(days=1)).isoformat() }}
{% else %}
{{ dl.isoformat() }}
{% endif %}
# ════════════════════════════════════════════════════════
# FIND CHEAPEST WINDOW
# ════════════════════════════════════════════════════════
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
must_finish_by: "{{ deadline }}"
response_variable: result
- if:
- condition: template
value_template: "{{ not result.window_found }}"
then:
- variables:
_n_title: "{{ title_no_cheap_slot }}"
_n_message: >
No cheap slot before
{{ deadline | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
for {{ duration }} min.
Run manually or extend the deadline.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_window
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
deadline: "{{ deadline }}"
duration_minutes: "{{ duration | int }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No cheap window found"
# ════════════════════════════════════════════════════════
# START WITH DELAY (device manages countdown)
# ════════════════════════════════════════════════════════
- variables:
_window_start: "{{ result.window.start | as_datetime }}"
start_in_relative: >
{{ [0, ((_window_start - now()).total_seconds()) | int] | max }}
# Dishwashers use StartInRelative = seconds until start
- action: home_connect.start_selected_program
target:
device_id: "{{ appliance_device }}"
data:
b_s_h_common_option_start_in_relative: "{{ start_in_relative }}"
- variables:
_n_title: "{{ title_planned }}"
_n_message: >
{% if start_in_relative | int > 0 %}
⏰ {{ _window_start | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
(in {{ (start_in_relative | int / 3600) | round(1) }} h)
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% else %}
▶️ Starting now!
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% endif %}
{% if _raw_duration in ['unknown', 'unavailable', 'None', ''] %}
· ⚠️ Duration estimated
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: planned
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
start_time: "{{ _window_start | as_local | as_timestamp | timestamp_custom('%Y-%m-%dT%H:%M:%S') }}"
duration_minutes: "{{ duration | int }}"
price_mean: "{{ result.window.price_mean | round(1) }}"
price_unit: "{{ result.price_unit }}"
using_fallback_duration: "{{ _raw_duration in ['unknown', 'unavailable', 'None', ''] }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"

View file

@ -0,0 +1,507 @@
blueprint:
name: "Tibber Prices: Dishwasher (Home Connect Alt)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v2.0.0
**Device-driven** dishwasher automation with electricity price
optimization using **Home Connect Alt**
([HACS integration by ekutner](https://my.home-assistant.io/redirect/hacs_repository/?owner=ekutner&repository=home-connect-hass&category=integration)).
**How it works:**
1. Select your program on the dishwasher
2. Close the door and enable Remote Start
3. The blueprint reads the program and estimated duration from the
device automatically
4. Finds the cheapest electricity window before your deadline
5. Tells the dishwasher when to start via `StartInRelative`
6. The dishwasher manages the countdown internally — no HA timers
**No scheduling needed** — the dishwasher handles the delayed start
itself. No `input_datetime` helpers required. Survives HA restarts
because the countdown runs on the appliance.
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- [Home Connect Alt](https://my.home-assistant.io/redirect/hacs_repository/?owner=ekutner&repository=home-connect-hass&category=integration) integration configured
- **Remote Start** enabled on the dishwasher
**Other variants:**
[Smart Plug](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher.yaml)
·
[Home Connect](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher_home_connect.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.11.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dishwasher_home_connect_alt.yaml
input:
appliance:
name: Appliance Entities
icon: mdi:dishwasher
description: >
Select your Home Connect Alt dishwasher entities.
All entities belong to the same appliance device.
input:
program_entity:
name: Program Select Entity
description: >
The **Programs** select entity of your dishwasher
(e.g., `select.dishwasher_programs`).
Used to read the selected program and as target for starting.
selector:
entity:
filter:
integration: home_connect_alt
domain: select
door_sensor:
name: Door Sensor
description: >
The door sensor of your dishwasher
(e.g., `binary_sensor.dishwasher_door`).
selector:
entity:
filter:
integration: home_connect_alt
domain: binary_sensor
device_class: door
remote_start_sensor:
name: Remote Control Sensor
description: >
The "Remote Control Active" binary sensor
(e.g., `binary_sensor.dishwasher_remote_control_active`).
Must be **on** for the automation to proceed.
selector:
entity:
filter:
integration: home_connect_alt
domain: binary_sensor
estimated_duration_entity:
name: Estimated Program Duration
description: >
The "Estimated Total Program Time" sensor
(e.g., `sensor.dishwasher_estimated_total_program_time`).
Shows the expected duration in `H:MM` format.
If unavailable, the fallback duration is used instead.
selector:
entity:
filter:
integration: home_connect_alt
domain: sensor
operation_state_entity:
name: Operation State
description: >
The "Operation State" sensor
(e.g., `sensor.dishwasher_operation_state`).
Used to verify the machine is ready before planning.
selector:
entity:
filter:
integration: home_connect_alt
domain: sensor
schedule:
name: Schedule
icon: mdi:calendar-clock
description: >
Configure the deadline and fallback duration.
input:
must_finish_by:
name: Must Finish By
description: >
The program must be finished by this time.
If this time has already passed today, the deadline
automatically moves to tomorrow (overnight mode).
default: "06:00:00"
selector:
time:
duration_fallback:
name: Fallback Duration (minutes)
description: >
Used **only** if the device doesn't report the estimated
duration (e.g., program not yet fully selected on the
appliance). Normally the duration is read automatically.
ECO 50°C ≈ 120 min, Auto ≈ 90 min, Intensive ≈ 150 min.
default: 120
selector:
number:
min: 30
max: 240
step: 5
unit_of_measurement: min
mode: slider
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional notifications. Use **simple mode** (just a service)
or point to an **advanced script** for multi-target,
presence-aware, and platform-specific notifications.
input:
notify_service:
name: Quick Notification (Simple)
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Ignored when the advanced script is set.
default: ""
selector:
text:
notification_script:
name: Notification Script (Advanced)
description: >
A `script.*` entity for advanced notifications
(multiple recipients, presence filtering, iOS/Android).
When set, replaces the simple notification.
Receives structured variables (event_type, appliance,
title, message, and context data).
default: ""
selector:
entity:
filter:
domain: script
title_setup_required:
name: "Title: Setup Required"
default: "🍽️ Dishwasher — Setup Required"
selector:
text:
title_not_ready:
name: "Title: Not Ready"
default: "🍽️ Dishwasher — Not Ready"
selector:
text:
title_no_program:
name: "Title: No Program"
default: "🍽️ Dishwasher — No Program"
selector:
text:
title_no_cheap_slot:
name: "Title: No Cheap Slot"
default: "🍽️ Dishwasher — No Cheap Slot"
selector:
text:
title_planned:
name: "Title: Planned"
default: "🍽️ Dishwasher — Planned!"
selector:
text:
mode: single
max_exceeded: silent
triggers:
# Fire when door closes OR remote start becomes active
- trigger: state
entity_id: !input door_sensor
to: "off"
- trigger: state
entity_id: !input remote_start_sensor
to: "on"
conditions:
# Both conditions must be true regardless of which trigger fired
- condition: state
entity_id: !input door_sensor
state: "off"
- condition: state
entity_id: !input remote_start_sensor
state: "on"
variables:
_blueprint_variant: "home_connect_alt"
program_entity: !input program_entity
door_sensor: !input door_sensor
remote_start_sensor: !input remote_start_sensor
estimated_duration_entity: !input estimated_duration_entity
operation_state_entity: !input operation_state_entity
must_finish_by_time: !input must_finish_by
duration_fallback: !input duration_fallback
notify_service: !input notify_service
notification_script: !input notification_script
title_setup_required: !input title_setup_required
title_not_ready: !input title_not_ready
title_no_program: !input title_no_program
title_no_cheap_slot: !input title_no_cheap_slot
title_planned: !input title_planned
actions:
# ════════════════════════════════════════════════════════
# PREFLIGHT CHECKS
# ════════════════════════════════════════════════════════
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- variables:
_n_title: "{{ title_setup_required }}"
_n_message: >
Install the Tibber Prices integration via HACS and
configure your Tibber account.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: setup_required
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Tibber Prices integration not found"
# Check: Machine is ready (not already running)?
- if:
- condition: template
value_template: >
{% set op = states(operation_state_entity) %}
{{ op not in ['unknown', 'unavailable']
and 'Ready' not in op
and 'Inactive' not in op }}
then:
- variables:
_n_title: "{{ title_not_ready }}"
_n_message: >
State: {{ states(operation_state_entity) }}.
Ensure it's idle with Remote Start enabled.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: not_ready
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Machine not ready"
# ════════════════════════════════════════════════════════
# READ DEVICE DATA
# ════════════════════════════════════════════════════════
- variables:
# Read selected program from device
selected_program: "{{ states(program_entity) }}"
# Read estimated duration from device (H:MM format → minutes)
_raw_duration: "{{ states(estimated_duration_entity) }}"
duration: >
{% set raw = states(estimated_duration_entity) %}
{% if raw not in ['unknown', 'unavailable', 'None', '']
and ':' in raw %}
{% set parts = raw.split(':') %}
{{ (parts[0] | int(0)) * 60 + (parts[1] | int(0)) }}
{% else %}
{{ duration_fallback }}
{% endif %}
# Compute deadline (auto-detect overnight)
deadline: >
{% set dl = today_at(must_finish_by_time) %}
{% if dl <= now() %}
{{ (dl + timedelta(days=1)).isoformat() }}
{% else %}
{{ dl.isoformat() }}
{% endif %}
# Validate program is selected
- if:
- condition: template
value_template: >
{{ selected_program in ['unknown', 'unavailable', 'None', ''] }}
then:
- variables:
_n_title: "{{ title_no_program }}"
_n_message: >
Select a program, close the door, and enable
Remote Start.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_program
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No program selected"
# ════════════════════════════════════════════════════════
# FIND CHEAPEST WINDOW
# ════════════════════════════════════════════════════════
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
must_finish_by: "{{ deadline }}"
response_variable: result
- if:
- condition: template
value_template: "{{ not result.window_found }}"
then:
- variables:
_n_title: "{{ title_no_cheap_slot }}"
_n_message: >
No cheap slot before
{{ deadline | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
for {{ duration }} min.
Run manually or extend the deadline.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_window
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
deadline: "{{ deadline }}"
duration_minutes: "{{ duration | int }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No cheap window found"
# ════════════════════════════════════════════════════════
# START WITH DELAY (device manages countdown)
# ════════════════════════════════════════════════════════
- variables:
_window_start: "{{ result.window.start | as_datetime }}"
# Dishwashers use StartInRelative (seconds until program starts)
start_in_relative: >
{{ [0, ((_window_start - now()).total_seconds()) | int] | max }}
- action: home_connect_alt.start_program
target:
entity_id: "{{ program_entity }}"
data:
program: "{{ selected_program }}"
options:
- key: BSH.Common.Option.StartInRelative
value: "{{ start_in_relative }}"
- variables:
_n_title: "{{ title_planned }}"
_n_message: >
{{ selected_program.split('.')[-1] }}
{% if start_in_relative | int > 0 %}
· ⏰ {{ _window_start | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
(in {{ (start_in_relative | int / 3600) | round(1) }} h)
{% else %}
· ▶️ Starting now!
{% endif %}
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% if _raw_duration in ['unknown', 'unavailable', 'None', ''] %}
· ⚠️ Duration estimated
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: planned
appliance: dishwasher
title: "{{ _n_title }}"
message: "{{ _n_message }}"
start_time: "{{ _window_start | as_local | as_timestamp | timestamp_custom('%Y-%m-%dT%H:%M:%S') }}"
duration_minutes: "{{ duration | int }}"
price_mean: "{{ result.window.price_mean | round(1) }}"
price_unit: "{{ result.price_unit }}"
selected_program: "{{ selected_program }}"
using_fallback_duration: "{{ _raw_duration in ['unknown', 'unavailable', 'None', ''] }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"

View file

@ -0,0 +1,261 @@
blueprint:
name: "Tibber Prices: Dryer (Smart Plug)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Automatically run your dryer at the cheapest electricity price
overnight using a smart plug.
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- One helper: Date & Time (`input_datetime`) — stores the planned start time
- Smart plug switch for the dryer
**Tip:** For multiple wash + dry cycles, use the
[Laundry Day Pipeline](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline.yaml)
blueprint instead.
**Other variants:**
[Home Connect](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer_home_connect.yaml)
·
[Home Connect Alt](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer_home_connect_alt.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer.yaml
input:
appliance:
name: Appliance
icon: mdi:tumble-dryer
description: Select the smart plug that controls your dryer.
input:
appliance_switch:
name: Dryer Smart Plug
description: The switch entity controlling the dryer.
selector:
entity:
filter:
domain: switch
schedule:
name: Schedule
icon: mdi:calendar-clock
description: Configure when to plan and the search window.
input:
plan_time:
name: Planning Time
description: >
When to search for the cheapest window each day.
Typically in the evening after loading the dryer.
default: "20:00:00"
selector:
time:
start_helper:
name: Start Time Helper
description: >
An `input_datetime` helper (type: Date and Time) that stores
the planned start time. Create in Settings → Helpers.
selector:
entity:
filter:
domain: input_datetime
duration:
name: Program Duration
description: >
Typical dry program duration in minutes.
Cotton Dry ≈ 60 min, Extra Dry ≈ 75 min, Gentle ≈ 90 min.
default: 65
selector:
number:
min: 15
max: 180
step: 5
unit_of_measurement: min
mode: slider
search_start:
name: Search Window Start
description: >
Earliest time the dryer may start.
Typically late evening.
default: "22:00:00"
selector:
time:
search_end:
name: Search Window End
description: >
Latest time the dryer must finish by.
The program must complete before this time.
default: "06:00:00"
selector:
time:
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect helpers to override settings from your
dashboard at runtime. When a helper is connected and has
a valid value, it takes priority over the fixed default.
Leave empty to always use the fixed defaults.
input:
duration_override:
name: "Override: Program Duration"
description: >
`input_number` helper to change the duration from your
dashboard without reconfiguring the blueprint.
**Create in Settings → Helpers → Number** with the same
min/max as the Duration slider above.
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional mobile notifications for planning and start events.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: time
at: !input plan_time
id: plan
- trigger: time
at: !input start_helper
id: execute
variables:
_blueprint_variant: "smart_plug"
appliance_switch: !input appliance_switch
start_helper: !input start_helper
_duration_default: !input duration
_duration_override: !input duration_override
duration: >
{% set o = _duration_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | int(_duration_default) }}
{% else %}
{{ _duration_default }}
{% endif %}
search_start: !input search_start
search_end: !input search_end
notify_service: !input notify_service
actions:
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🌀 Dryer — Setup Required"
message: >
The Tibber Prices integration is not installed.
- stop: "Tibber Prices integration not found"
- choose:
- conditions:
- condition: trigger
id: plan
sequence:
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
search_start_time: "{{ search_start }}"
search_end_time: "{{ search_end }}"
search_end_day_offset: 1
response_variable: result
- if:
- condition: template
value_template: "{{ result.window_found }}"
then:
- action: input_datetime.set_datetime
target:
entity_id: "{{ start_helper }}"
data:
datetime: "{{ result.window.start }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🌀 Dryer Planned"
message: >
Start at {{ result.window.start | as_datetime
| as_local | as_timestamp
| timestamp_custom('%H:%M') }}.
Avg price: {{ result.window.price_mean | round(1) }}
{{ result.price_unit }}/kWh.
else:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🌀 Dryer"
message: No cheap window found. Consider running manually.
- conditions:
- condition: trigger
id: execute
sequence:
- action: switch.turn_on
target:
entity_id: "{{ appliance_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🌀 Dryer Started"
message: >
Smart plug turned on. Program should finish in
~{{ duration }} minutes.

View file

@ -0,0 +1,458 @@
blueprint:
name: "Tibber Prices: Dryer (Home Connect)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v2.0.0
**Device-driven** dryer automation with electricity price
optimization using the **Home Connect** integration (HA Core).
**How it works:**
1. Select your program on the dryer
2. Close the door and enable Remote Start
3. The blueprint reads the estimated duration from the device
4. Finds the cheapest electricity window before your deadline
5. Tells the dryer when to finish via `FinishInRelative`
6. The dryer calculates when to start and manages the countdown
internally — no HA timers
**Important:** Dryers use `FinishInRelative` (like washing machines).
The appliance receives the deadline and calculates the optimal start
time itself.
**No scheduling needed** — the dryer handles the delayed start
itself. No `input_datetime` helpers required. Survives HA restarts
because the countdown runs on the appliance.
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- [Home Connect](https://www.home-assistant.io/integrations/home_connect/) integration configured
- **Remote Start** enabled on the dryer
**Tip:** For multiple wash + dry cycles, use the
[Laundry Day Pipeline](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline_home_connect.yaml)
blueprint instead.
**Other variants:**
[Smart Plug](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer.yaml)
·
[Home Connect Alt](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer_home_connect_alt.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.11.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer_home_connect.yaml
input:
appliance:
name: Appliance
icon: mdi:tumble-dryer
description: >
Select your Home Connect dryer device and entities.
input:
appliance_device:
name: Dryer Device
description: >
Your dryer from the Home Connect integration.
Used to target the start command.
selector:
device:
filter:
integration: home_connect
door_sensor:
name: Door Sensor
description: >
The door sensor of your dryer.
selector:
entity:
filter:
integration: home_connect
domain: binary_sensor
device_class: door
remote_start_sensor:
name: Remote Control Sensor
description: >
The "Remote Control Active" binary sensor.
Must be **on** for the automation to proceed.
selector:
entity:
filter:
integration: home_connect
domain: binary_sensor
estimated_duration_entity:
name: Estimated Program Duration
description: >
The "Estimated Total Program Time" sensor.
If unavailable, the fallback duration is used instead.
selector:
entity:
filter:
integration: home_connect
domain: sensor
operation_state_entity:
name: Operation State
description: >
The "Operation State" sensor.
Used to verify the machine is ready before planning.
selector:
entity:
filter:
integration: home_connect
domain: sensor
schedule:
name: Schedule
icon: mdi:calendar-clock
description: >
Configure the deadline and fallback duration.
input:
must_finish_by:
name: Must Finish By
description: >
The program must be finished by this time.
If this time has already passed today, the deadline
automatically moves to tomorrow (overnight mode).
default: "06:00:00"
selector:
time:
duration_fallback:
name: Fallback Duration (minutes)
description: >
Used **only** if the device doesn't report the estimated
duration. Normally the duration is read automatically.
Cotton Dry ≈ 60 min, Extra Dry ≈ 75 min, Gentle ≈ 90 min.
default: 65
selector:
number:
min: 15
max: 180
step: 5
unit_of_measurement: min
mode: slider
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional notifications. Use **simple mode** (just a service)
or point to an **advanced script** for multi-target,
presence-aware, and platform-specific notifications.
input:
notify_service:
name: Quick Notification (Simple)
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Ignored when the advanced script is set.
default: ""
selector:
text:
notification_script:
name: Notification Script (Advanced)
description: >
A `script.*` entity for advanced notifications
(multiple recipients, presence filtering, iOS/Android).
When set, replaces the simple notification.
Receives structured variables (event_type, appliance,
title, message, and context data).
default: ""
selector:
entity:
filter:
domain: script
title_setup_required:
name: "Title: Setup Required"
default: "🌀 Dryer — Setup Required"
selector:
text:
title_not_ready:
name: "Title: Not Ready"
default: "🌀 Dryer — Not Ready"
selector:
text:
title_no_cheap_slot:
name: "Title: No Cheap Slot"
default: "🌀 Dryer — No Cheap Slot"
selector:
text:
title_planned:
name: "Title: Planned"
default: "🌀 Dryer — Planned!"
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: state
entity_id: !input door_sensor
to: "off"
- trigger: state
entity_id: !input remote_start_sensor
to: "on"
conditions:
- condition: state
entity_id: !input door_sensor
state: "off"
- condition: state
entity_id: !input remote_start_sensor
state: "on"
variables:
_blueprint_variant: "home_connect"
appliance_device: !input appliance_device
door_sensor: !input door_sensor
remote_start_sensor: !input remote_start_sensor
estimated_duration_entity: !input estimated_duration_entity
operation_state_entity: !input operation_state_entity
must_finish_by_time: !input must_finish_by
duration_fallback: !input duration_fallback
notify_service: !input notify_service
notification_script: !input notification_script
title_setup_required: !input title_setup_required
title_not_ready: !input title_not_ready
title_no_cheap_slot: !input title_no_cheap_slot
title_planned: !input title_planned
actions:
# ════════════════════════════════════════════════════════
# PREFLIGHT CHECKS
# ════════════════════════════════════════════════════════
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- variables:
_n_title: "{{ title_setup_required }}"
_n_message: >
Install the Tibber Prices integration via HACS and
configure your Tibber account.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: setup_required
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Tibber Prices integration not found"
- if:
- condition: template
value_template: >
{% set op = states(operation_state_entity) %}
{{ op not in ['unknown', 'unavailable']
and 'Ready' not in op
and 'Inactive' not in op }}
then:
- variables:
_n_title: "{{ title_not_ready }}"
_n_message: >
State: {{ states(operation_state_entity) }}.
Ensure it's idle with Remote Start enabled.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: not_ready
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Machine not ready"
# ════════════════════════════════════════════════════════
# READ DEVICE DATA
# ════════════════════════════════════════════════════════
- variables:
_raw_duration: "{{ states(estimated_duration_entity) }}"
duration: >
{% set raw = states(estimated_duration_entity) %}
{% if raw not in ['unknown', 'unavailable', 'None', '']
and ':' in raw %}
{% set parts = raw.split(':') %}
{{ (parts[0] | int(0)) * 60 + (parts[1] | int(0)) }}
{% elif raw not in ['unknown', 'unavailable', 'None', '']
and raw | int(0) > 0 %}
{{ raw | int }}
{% else %}
{{ duration_fallback }}
{% endif %}
deadline: >
{% set dl = today_at(must_finish_by_time) %}
{% if dl <= now() %}
{{ (dl + timedelta(days=1)).isoformat() }}
{% else %}
{{ dl.isoformat() }}
{% endif %}
# ════════════════════════════════════════════════════════
# FIND CHEAPEST WINDOW
# ════════════════════════════════════════════════════════
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
must_finish_by: "{{ deadline }}"
response_variable: result
- if:
- condition: template
value_template: "{{ not result.window_found }}"
then:
- variables:
_n_title: "{{ title_no_cheap_slot }}"
_n_message: >
No cheap slot before
{{ deadline | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
for {{ duration }} min.
Run manually or extend the deadline.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_window
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
deadline: "{{ deadline }}"
duration_minutes: "{{ duration | int }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No cheap window found"
# ════════════════════════════════════════════════════════
# START WITH DELAY (device manages countdown)
# ════════════════════════════════════════════════════════
- variables:
_window_start: "{{ result.window.start | as_datetime }}"
_window_end: >
{{ (_window_start + timedelta(minutes=duration | int)).isoformat() }}
finish_in_relative: >
{% set window_end = _window_start + timedelta(minutes=duration | int) %}
{% set seconds_until_end = ((window_end - now()).total_seconds()) | int %}
{{ [duration | int * 60, seconds_until_end] | max }}
# Dryers use FinishInRelative
- action: home_connect.set_program_and_options
target:
device_id: "{{ appliance_device }}"
data:
affects_to: active_program
b_s_h_common_option_finish_in_relative: "{{ finish_in_relative }}"
- variables:
_n_title: "{{ title_planned }}"
_n_message: >
{% set delay = finish_in_relative | int - (duration | int * 60) %}
{% if delay > 0 %}
⏰ ~{{ _window_start | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
(in {{ (delay / 3600) | round(1) }} h)
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% else %}
▶️ Starting now!
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% endif %}
{% if _raw_duration in ['unknown', 'unavailable', 'None', ''] %}
· ⚠️ Duration estimated
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: planned
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
start_time: "{{ _window_start | as_local | as_timestamp | timestamp_custom('%Y-%m-%dT%H:%M:%S') }}"
duration_minutes: "{{ duration | int }}"
price_mean: "{{ result.window.price_mean | round(1) }}"
price_unit: "{{ result.price_unit }}"
using_fallback_duration: "{{ _raw_duration in ['unknown', 'unavailable', 'None', ''] }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"

View file

@ -0,0 +1,510 @@
blueprint:
name: "Tibber Prices: Dryer (Home Connect Alt)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v2.0.0
**Device-driven** dryer automation with electricity price
optimization using **Home Connect Alt**
([HACS integration by ekutner](https://my.home-assistant.io/redirect/hacs_repository/?owner=ekutner&repository=home-connect-hass&category=integration)).
**How it works:**
1. Select your program on the dryer
2. Close the door and enable Remote Start
3. The blueprint reads the program and estimated duration from the
device automatically
4. Finds the cheapest electricity window before your deadline
5. Tells the dryer when to finish via `FinishInRelative`
6. The dryer calculates when to start and manages the countdown
internally — no HA timers
**Important:** Dryers use `FinishInRelative` (like washing machines).
The appliance receives the deadline and calculates the optimal start
time itself.
**No scheduling needed** — the dryer handles the delayed start
itself. No `input_datetime` helpers required. Survives HA restarts
because the countdown runs on the appliance.
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- [Home Connect Alt](https://my.home-assistant.io/redirect/hacs_repository/?owner=ekutner&repository=home-connect-hass&category=integration) integration configured
- **Remote Start** enabled on the dryer
**Tip:** For multiple wash + dry cycles, use the
[Laundry Day Pipeline](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline_home_connect_alt.yaml)
blueprint instead.
**Other variants:**
[Smart Plug](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer.yaml)
·
[Home Connect](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer_home_connect.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.11.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/dryer_home_connect_alt.yaml
input:
appliance:
name: Appliance Entities
icon: mdi:tumble-dryer
description: >
Select your Home Connect Alt dryer entities.
All entities belong to the same appliance device.
input:
program_entity:
name: Program Select Entity
description: >
The **Programs** select entity of your dryer
(e.g., `select.dryer_programs`).
Used to read the selected program and as target for starting.
selector:
entity:
filter:
integration: home_connect_alt
domain: select
door_sensor:
name: Door Sensor
description: >
The door sensor of your dryer
(e.g., `binary_sensor.dryer_door`).
selector:
entity:
filter:
integration: home_connect_alt
domain: binary_sensor
device_class: door
remote_start_sensor:
name: Remote Control Sensor
description: >
The "Remote Control Active" binary sensor
(e.g., `binary_sensor.dryer_remote_control_active`).
Must be **on** for the automation to proceed.
selector:
entity:
filter:
integration: home_connect_alt
domain: binary_sensor
estimated_duration_entity:
name: Estimated Program Duration
description: >
The "Estimated Total Program Time" sensor
(e.g., `sensor.dryer_estimated_total_program_time`).
Shows the expected duration in `H:MM` format.
If unavailable, the fallback duration is used instead.
selector:
entity:
filter:
integration: home_connect_alt
domain: sensor
operation_state_entity:
name: Operation State
description: >
The "Operation State" sensor
(e.g., `sensor.dryer_operation_state`).
Used to verify the machine is ready before planning.
selector:
entity:
filter:
integration: home_connect_alt
domain: sensor
schedule:
name: Schedule
icon: mdi:calendar-clock
description: >
Configure the deadline and fallback duration.
input:
must_finish_by:
name: Must Finish By
description: >
The program must be finished by this time.
If this time has already passed today, the deadline
automatically moves to tomorrow (overnight mode).
default: "06:00:00"
selector:
time:
duration_fallback:
name: Fallback Duration (minutes)
description: >
Used **only** if the device doesn't report the estimated
duration (e.g., program not yet fully selected on the
appliance). Normally the duration is read automatically.
Cotton Dry ≈ 60 min, Extra Dry ≈ 75 min, Gentle ≈ 90 min.
default: 65
selector:
number:
min: 15
max: 180
step: 5
unit_of_measurement: min
mode: slider
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional notifications. Use **simple mode** (just a service)
or point to an **advanced script** for multi-target,
presence-aware, and platform-specific notifications.
input:
notify_service:
name: Quick Notification (Simple)
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Ignored when the advanced script is set.
default: ""
selector:
text:
notification_script:
name: Notification Script (Advanced)
description: >
A `script.*` entity for advanced notifications
(multiple recipients, presence filtering, iOS/Android).
When set, replaces the simple notification.
Receives structured variables (event_type, appliance,
title, message, and context data).
default: ""
selector:
entity:
filter:
domain: script
title_setup_required:
name: "Title: Setup Required"
default: "🌀 Dryer — Setup Required"
selector:
text:
title_not_ready:
name: "Title: Not Ready"
default: "🌀 Dryer — Not Ready"
selector:
text:
title_no_program:
name: "Title: No Program"
default: "🌀 Dryer — No Program"
selector:
text:
title_no_cheap_slot:
name: "Title: No Cheap Slot"
default: "🌀 Dryer — No Cheap Slot"
selector:
text:
title_planned:
name: "Title: Planned"
default: "🌀 Dryer — Planned!"
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: state
entity_id: !input door_sensor
to: "off"
- trigger: state
entity_id: !input remote_start_sensor
to: "on"
conditions:
- condition: state
entity_id: !input door_sensor
state: "off"
- condition: state
entity_id: !input remote_start_sensor
state: "on"
variables:
_blueprint_variant: "home_connect_alt"
program_entity: !input program_entity
door_sensor: !input door_sensor
remote_start_sensor: !input remote_start_sensor
estimated_duration_entity: !input estimated_duration_entity
operation_state_entity: !input operation_state_entity
must_finish_by_time: !input must_finish_by
duration_fallback: !input duration_fallback
notify_service: !input notify_service
notification_script: !input notification_script
title_setup_required: !input title_setup_required
title_not_ready: !input title_not_ready
title_no_program: !input title_no_program
title_no_cheap_slot: !input title_no_cheap_slot
title_planned: !input title_planned
actions:
# ════════════════════════════════════════════════════════
# PREFLIGHT CHECKS
# ════════════════════════════════════════════════════════
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- variables:
_n_title: "{{ title_setup_required }}"
_n_message: >
Install the Tibber Prices integration via HACS and
configure your Tibber account.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: setup_required
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Tibber Prices integration not found"
- if:
- condition: template
value_template: >
{% set op = states(operation_state_entity) %}
{{ op not in ['unknown', 'unavailable']
and 'Ready' not in op
and 'Inactive' not in op }}
then:
- variables:
_n_title: "{{ title_not_ready }}"
_n_message: >
State: {{ states(operation_state_entity) }}.
Ensure it's idle with Remote Start enabled.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: not_ready
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Machine not ready"
# ════════════════════════════════════════════════════════
# READ DEVICE DATA
# ════════════════════════════════════════════════════════
- variables:
selected_program: "{{ states(program_entity) }}"
_raw_duration: "{{ states(estimated_duration_entity) }}"
duration: >
{% set raw = states(estimated_duration_entity) %}
{% if raw not in ['unknown', 'unavailable', 'None', '']
and ':' in raw %}
{% set parts = raw.split(':') %}
{{ (parts[0] | int(0)) * 60 + (parts[1] | int(0)) }}
{% else %}
{{ duration_fallback }}
{% endif %}
deadline: >
{% set dl = today_at(must_finish_by_time) %}
{% if dl <= now() %}
{{ (dl + timedelta(days=1)).isoformat() }}
{% else %}
{{ dl.isoformat() }}
{% endif %}
- if:
- condition: template
value_template: >
{{ selected_program in ['unknown', 'unavailable', 'None', ''] }}
then:
- variables:
_n_title: "{{ title_no_program }}"
_n_message: >
Select a program, close the door, and enable
Remote Start.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_program
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No program selected"
# ════════════════════════════════════════════════════════
# FIND CHEAPEST WINDOW
# ════════════════════════════════════════════════════════
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
must_finish_by: "{{ deadline }}"
response_variable: result
- if:
- condition: template
value_template: "{{ not result.window_found }}"
then:
- variables:
_n_title: "{{ title_no_cheap_slot }}"
_n_message: >
No cheap slot before
{{ deadline | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
for {{ duration }} min.
Run manually or extend the deadline.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_window
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
deadline: "{{ deadline }}"
duration_minutes: "{{ duration | int }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No cheap window found"
# ════════════════════════════════════════════════════════
# START WITH DELAY (device manages countdown)
# ════════════════════════════════════════════════════════
- variables:
_window_start: "{{ result.window.start | as_datetime }}"
_window_end: >
{{ (_window_start + timedelta(minutes=duration | int)).isoformat() }}
finish_in_relative: >
{% set window_end = _window_start + timedelta(minutes=duration | int) %}
{% set seconds_until_end = ((window_end - now()).total_seconds()) | int %}
{{ [duration | int * 60, seconds_until_end] | max }}
- action: home_connect_alt.start_program
target:
entity_id: "{{ program_entity }}"
data:
program: "{{ selected_program }}"
options:
- key: BSH.Common.Option.FinishInRelative
value: "{{ finish_in_relative }}"
- variables:
_n_title: "{{ title_planned }}"
_n_message: >
{{ selected_program.split('.')[-1] }}
{% set delay = finish_in_relative | int - (duration | int * 60) %}
{% if delay > 0 %}
· ⏰ ~{{ _window_start | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
(in {{ (delay / 3600) | round(1) }} h)
{% else %}
· ▶️ Starting now!
{% endif %}
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% if _raw_duration in ['unknown', 'unavailable', 'None', ''] %}
· ⚠️ Duration estimated
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: planned
appliance: dryer
title: "{{ _n_title }}"
message: "{{ _n_message }}"
start_time: "{{ _window_start | as_local | as_timestamp | timestamp_custom('%Y-%m-%dT%H:%M:%S') }}"
duration_minutes: "{{ duration | int }}"
price_mean: "{{ result.window.price_mean | round(1) }}"
price_unit: "{{ result.price_unit }}"
using_fallback_duration: "{{ _raw_duration in ['unknown', 'unavailable', 'None', ''] }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"

View file

@ -0,0 +1,301 @@
blueprint:
name: "Tibber Prices: EV Charging — Cheapest Hours Overnight"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Automatically charge your electric vehicle during the cheapest hours
overnight. Uses `find_cheapest_hours` to select the cheapest
individual 15-minute intervals — the charger may pause and resume
between segments.
**What it does:**
- Finds the cheapest intervals within a configurable search window
- Stores the first segment's start time in a helper
- Turns the charger on/off based on an interval schedule
- Optional: Skips planning if battery is already above a threshold
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- One helper: Date & Time (`input_datetime`) for the charge start
- A smart plug or charger switch entity
**Alternative:** If your charger can't pause/resume, use
`find_cheapest_block` instead (see the Dishwasher Smart Plug
blueprint for a contiguous-window example).
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/ev_charging.yaml
input:
vehicle:
name: Vehicle / Charger
icon: mdi:ev-station
description: Configure your EV charger switch and optional battery sensor.
input:
charger_switch:
name: Charger Switch
description: >
The switch entity that controls your EV charger
(smart plug or charger integration).
selector:
entity:
filter:
domain: switch
battery_sensor:
name: Battery Level Sensor (optional)
description: >
If provided, charging is only planned when the battery
is below the threshold. Leave empty to always plan.
default: ""
selector:
entity:
filter:
domain: sensor
device_class: battery
battery_threshold:
name: Battery Threshold
description: >
Only plan charging if battery level is below this
percentage. Ignored if no battery sensor is selected.
default: 80
selector:
number:
min: 10
max: 100
step: 5
unit_of_measurement: "%"
schedule:
name: Schedule
icon: mdi:calendar-clock
description: Configure charging times and the overnight search window.
input:
plan_time:
name: Planning Time
description: >
When to search for the cheapest hours each day.
Should be before the search window starts.
default: "18:00:00"
selector:
time:
charge_duration:
name: Total Charging Duration
description: >
How many hours of cheap charging to find.
default: "04:00:00"
selector:
time:
min_segment:
name: Minimum Segment Duration
description: >
Shortest uninterrupted charging segment. Prevents
very short on/off cycles that stress the charger.
default: "00:30:00"
selector:
time:
search_start:
name: Search Window Start
description: >
Earliest time charging may begin.
default: "18:00:00"
selector:
time:
search_end:
name: Search Window End
description: >
Latest time charging must finish by.
The vehicle should be ready by this time.
default: "07:00:00"
selector:
time:
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect helpers to override settings from your
dashboard at runtime. When a helper is connected and has
a valid value, it takes priority over the fixed default.
Leave empty to always use the fixed defaults.
input:
charge_duration_override:
name: "Override: Charging Duration"
description: >
`input_number` helper to change the charging duration
(in hours) from your dashboard. Useful when daily
charging needs vary.
**Create in Settings → Helpers → Number**
(min: 0.5, max: 12, step: 0.5, unit: h).
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional mobile notifications for charging schedule
and start/stop events.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: time
at: !input plan_time
id: plan
variables:
_blueprint_variant: "ev_charging"
charger_switch: !input charger_switch
battery_sensor: !input battery_sensor
battery_threshold: !input battery_threshold
_charge_duration_default: !input charge_duration
_charge_duration_override: !input charge_duration_override
charge_duration: >
{% set o = _charge_duration_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{% set hours = states(o) | float(4) %}
{{ '%02d:%02d:00' | format(hours | int, ((hours % 1) * 60) | int) }}
{% else %}
{{ _charge_duration_default }}
{% endif %}
min_segment: !input min_segment
search_start: !input search_start
search_end: !input search_end
notify_service: !input notify_service
actions:
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔌 EV Charging — Setup Required"
message: The Tibber Prices integration is not installed.
- stop: "Tibber Prices integration not found"
# ════════════════════════════════════════════════════════
# BATTERY CHECK
# ════════════════════════════════════════════════════════
- if:
- condition: template
value_template: >
{{ battery_sensor | length > 0
and states(battery_sensor) | int(0) >= battery_threshold | int }}
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔌 EV Charging Skipped"
message: >
Battery at {{ states(battery_sensor) }}% (threshold:
{{ battery_threshold }}%). No charging needed.
- stop: "Battery above threshold"
# ════════════════════════════════════════════════════════
# FIND CHEAPEST HOURS
# ════════════════════════════════════════════════════════
- action: tibber_prices.find_cheapest_hours
data:
duration: "{{ charge_duration }}"
min_segment_duration: "{{ min_segment }}"
search_start_time: "{{ search_start }}"
search_end_time: "{{ search_end }}"
search_end_day_offset: 1
response_variable: result
- if:
- condition: template
value_template: "{{ result.intervals_found }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔌 EV Charging Planned"
message: >
{{ result.schedule.segment_count }} charging sessions:
{% for seg in result.schedule.segments %}
• {{ seg.start | as_datetime | as_local | as_timestamp
| timestamp_custom('%H:%M') }}{{ seg.end | as_datetime
| as_local | as_timestamp | timestamp_custom('%H:%M') }}
({{ seg.price_mean | round(1) }} {{ result.price_unit }})
{% endfor %}
# Turn on/off charger for each segment
- repeat:
for_each: "{{ result.schedule.segments }}"
sequence:
- delay: >
{{ ((repeat.item.start | as_datetime | as_local
| as_timestamp) - (now() | as_timestamp)) | int }}
- action: switch.turn_on
target:
entity_id: "{{ charger_switch }}"
- delay: >
{{ ((repeat.item.end | as_datetime | as_local
| as_timestamp) - (repeat.item.start | as_datetime
| as_local | as_timestamp)) | int }}
- action: switch.turn_off
target:
entity_id: "{{ charger_switch }}"
else:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔌 EV Charging"
message: No cheap intervals found in the search window.

View file

@ -0,0 +1,235 @@
blueprint:
name: "Tibber Prices: Heat Pump — Temperature by Price Level"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Adjust your heat pump target temperature based on the current
electricity price rating. Higher target when cheap, lower when
expensive — the simplest real-time heat pump optimization.
**What it does:**
- Reacts every 15 minutes when the price sensor updates
- Sets one of 5 target temperatures based on `rating_level`
(VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE)
- No helpers needed — pure sensor-based
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- A `climate.*` entity for your heat pump
**See also:**
[Heat Pump Smart Boost](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/heat_pump_smart_boost.yaml)
— a more advanced variant that extends boost during V-shaped
price valleys using trend awareness.
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/heat_pump_price_level.yaml
input:
devices:
name: Devices
icon: mdi:heat-pump-outline
description: Select your heat pump and the Tibber Prices sensor.
input:
price_sensor:
name: Current Price Sensor
description: >
The `sensor.<home>_current_electricity_price` from
Tibber Prices. Must have `rating_level` attribute.
selector:
entity:
filter:
domain: sensor
integration: tibber_prices
heat_pump_entity:
name: Heat Pump
description: Your heat pump climate entity.
selector:
entity:
filter:
domain: climate
temperatures:
name: Target Temperatures
icon: mdi:thermometer
description: >
Set the target temperature for each price level.
Temperatures are in °C.
input:
temp_very_cheap:
name: VERY_CHEAP Temperature
description: Maximum comfort when prices are very low.
default: 23.0
selector:
number:
min: 15
max: 30
step: 0.5
unit_of_measurement: °C
temp_cheap:
name: CHEAP Temperature
description: Slightly above normal for moderate savings.
default: 22.0
selector:
number:
min: 15
max: 30
step: 0.5
unit_of_measurement: °C
temp_normal:
name: NORMAL Temperature
description: Baseline comfort temperature for average prices.
default: 20.5
selector:
number:
min: 15
max: 30
step: 0.5
unit_of_measurement: °C
temp_expensive:
name: EXPENSIVE Temperature
description: Reduced temperature to save during high prices.
default: 19.0
selector:
number:
min: 15
max: 30
step: 0.5
unit_of_measurement: °C
temp_very_expensive:
name: VERY_EXPENSIVE Temperature
description: Minimum to save energy during peak prices.
default: 18.0
selector:
number:
min: 15
max: 30
step: 0.5
unit_of_measurement: °C
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect a helper to shift all target temperatures
at once (e.g., +2°C comfort boost in winter, 1°C in summer).
Leave empty to always use the fixed defaults.
input:
temperature_offset_override:
name: "Override: Temperature Offset"
description: >
`input_number` helper to shift ALL target temperatures
up or down from your dashboard.
**Create in Settings → Helpers → Number**
(min: 5, max: 5, step: 0.5, unit: °C).
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional mobile notifications for temperature adjustments.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
mode: restart
triggers:
- trigger: state
entity_id: !input price_sensor
variables:
_blueprint_variant: "heat_pump_price_level"
price_sensor: !input price_sensor
heat_pump_entity: !input heat_pump_entity
_temp_vc: !input temp_very_cheap
_temp_c: !input temp_cheap
_temp_n: !input temp_normal
_temp_e: !input temp_expensive
_temp_ve: !input temp_very_expensive
_temp_offset_override: !input temperature_offset_override
_temp_offset: >
{% set o = _temp_offset_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | float(0) }}
{% else %}
0
{% endif %}
temp_very_cheap: "{{ (_temp_vc | float) + (_temp_offset | float) }}"
temp_cheap: "{{ (_temp_c | float) + (_temp_offset | float) }}"
temp_normal: "{{ (_temp_n | float) + (_temp_offset | float) }}"
temp_expensive: "{{ (_temp_e | float) + (_temp_offset | float) }}"
temp_very_expensive: "{{ (_temp_ve | float) + (_temp_offset | float) }}"
notify_service: !input notify_service
level: >
{{ state_attr(price_sensor, 'rating_level') | default('NORMAL') }}
actions:
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- stop: "Tibber Prices integration not found"
# ════════════════════════════════════════════════════════
# SET TEMPERATURE BASED ON PRICE LEVEL
# ════════════════════════════════════════════════════════
- variables:
target_temp: >
{% if level == 'VERY_CHEAP' %}
{{ temp_very_cheap }}
{% elif level == 'CHEAP' %}
{{ temp_cheap }}
{% elif level == 'EXPENSIVE' %}
{{ temp_expensive }}
{% elif level == 'VERY_EXPENSIVE' %}
{{ temp_very_expensive }}
{% else %}
{{ temp_normal }}
{% endif %}
- action: climate.set_temperature
target:
entity_id: "{{ heat_pump_entity }}"
data:
temperature: "{{ target_temp | float }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🌡️ Heat Pump Adjusted"
message: >
Price level: {{ level }}. Target temperature set to
{{ target_temp }}°C.

View file

@ -0,0 +1,282 @@
blueprint:
name: "Tibber Prices: Heat Pump — Smart Boost with Trend Awareness"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Advanced heat pump optimization that extends the boost window
beyond the detected Best Price Period using trend sensors.
**Why?** On V-shaped price days, the Best Price Period may cover
only 12 hours, but prices remain favorable for 46 hours. By
checking the price level AND the trend, you can safely boost
during the entire cheap valley.
**Logic:**
- **Boost** when EITHER: (a) inside a Best Price Period, OR
(b) price is CHEAP/VERY_CHEAP AND trend is stable/falling
- **Return to normal** when NEITHER condition is true
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- A `climate.*` entity for your heat pump
**See also:**
[Heat Pump Price Level](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/heat_pump_price_level.yaml)
— simpler variant that adjusts to 5 different temperatures per
price level without trend awareness.
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/heat_pump_smart_boost.yaml
input:
devices:
name: Devices
icon: mdi:heat-pump-outline
description: >
Select your heat pump and the Tibber Prices sensors.
input:
period_sensor:
name: Best Price Period Sensor
description: >
The `binary_sensor.<home>_best_price_period` from
Tibber Prices.
selector:
entity:
filter:
domain: binary_sensor
integration: tibber_prices
price_sensor:
name: Current Price Sensor
description: >
The `sensor.<home>_current_electricity_price` from
Tibber Prices. Must have `rating_level` attribute.
selector:
entity:
filter:
domain: sensor
integration: tibber_prices
trend_sensor:
name: Price Outlook Sensor (1h)
description: >
The `sensor.<home>_price_outlook_1h` from Tibber Prices.
Must have `trend_value` attribute. `rising` means current
price is LOWER than the future average — so it's actually
a good time to boost.
selector:
entity:
filter:
domain: sensor
integration: tibber_prices
heat_pump_entity:
name: Heat Pump
description: Your heat pump climate entity.
selector:
entity:
filter:
domain: climate
temperatures:
name: Temperatures
icon: mdi:thermometer
description: Boost and normal target temperatures.
input:
boost_temperature:
name: Boost Temperature
description: Target during the extended cheap window.
default: 22.0
selector:
number:
min: 15
max: 30
step: 0.5
unit_of_measurement: °C
normal_temperature:
name: Normal Temperature
description: Target when no cheap conditions apply.
default: 20.5
selector:
number:
min: 15
max: 30
step: 0.5
unit_of_measurement: °C
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect helpers to override settings from your
dashboard at runtime. When a helper is connected and has
a valid value, it takes priority over the fixed default.
Leave empty to always use the fixed defaults.
input:
boost_temperature_override:
name: "Override: Boost Temperature"
description: >
`input_number` helper to change the boost temperature
from your dashboard.
**Create in Settings → Helpers → Number**
(min: 15, max: 30, step: 0.5, unit: °C).
default: ""
selector:
entity:
filter:
domain: input_number
normal_temperature_override:
name: "Override: Normal Temperature"
description: >
`input_number` helper to change the normal temperature
from your dashboard.
**Create in Settings → Helpers → Number**
(min: 15, max: 30, step: 0.5, unit: °C).
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional mobile notifications for boost start/stop events.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
mode: restart
triggers:
# Best price period starts/stops
- trigger: state
entity_id: !input period_sensor
to: "on"
id: period_start
- trigger: state
entity_id: !input period_sensor
to: "off"
id: period_end
# Price updates every 15 minutes
- trigger: state
entity_id: !input price_sensor
id: price_update
variables:
_blueprint_variant: "heat_pump_smart_boost"
period_sensor: !input period_sensor
price_sensor: !input price_sensor
trend_sensor: !input trend_sensor
heat_pump_entity: !input heat_pump_entity
_boost_temp_default: !input boost_temperature
_boost_temp_override: !input boost_temperature_override
boost_temperature: >
{% set o = _boost_temp_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | float(_boost_temp_default) }}
{% else %}
{{ _boost_temp_default }}
{% endif %}
_normal_temp_default: !input normal_temperature
_normal_temp_override: !input normal_temperature_override
normal_temperature: >
{% set o = _normal_temp_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | float(_normal_temp_default) }}
{% else %}
{{ _normal_temp_default }}
{% endif %}
notify_service: !input notify_service
actions:
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- stop: "Tibber Prices integration not found"
# ════════════════════════════════════════════════════════
# EVALUATE BOOST CONDITIONS
# ════════════════════════════════════════════════════════
- variables:
in_period: >
{{ is_state(period_sensor, 'on') }}
is_cheap: >
{{ state_attr(price_sensor, 'rating_level')
| default('NORMAL') in ['VERY_CHEAP', 'CHEAP'] }}
trend_ok: >
{{ state_attr(trend_sensor, 'trend_value')
| int(0) <= 0 }}
should_boost: >
{{ in_period or (is_cheap and trend_ok) }}
- choose:
# ── BOOST ──
- conditions:
- condition: template
value_template: "{{ should_boost }}"
sequence:
- action: climate.set_temperature
target:
entity_id: "{{ heat_pump_entity }}"
data:
temperature: "{{ boost_temperature | float }}"
- if:
- condition: template
value_template: >
{{ notify_service | length > 0
and trigger.id == 'period_start' }}
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🌡️ Heat Pump — Boost Active"
message: >
{% if in_period %}Best price period started.
{% else %}Price is cheap and trend is favorable.
{% endif %}
Target set to {{ boost_temperature }}°C.
# ── RETURN TO NORMAL ──
default:
- action: climate.set_temperature
target:
entity_id: "{{ heat_pump_entity }}"
data:
temperature: "{{ normal_temperature | float }}"
- if:
- condition: template
value_template: >
{{ notify_service | length > 0
and trigger.id == 'period_end' }}
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🌡️ Heat Pump — Normal Mode"
message: >
Cheap window ended. Target back to
{{ normal_temperature }}°C.

View file

@ -0,0 +1,390 @@
blueprint:
name: "Tibber Prices: Home Battery — Charge Cheap, Discharge Expensive"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Optimize your home battery by charging from the grid during cheap
prices and discharging during expensive periods.
**What it does:**
- **Best Price Period ON** → Charge from grid (if SOC below threshold)
- **Peak Price Period ON** → Discharge to grid (if SOC above threshold)
- **Both OFF** → Stop grid charging/discharging (solar-only mode)
- Optional: Volatility check — skip charging on flat-price days
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- Switch entities for grid charging and grid discharge
- Optional: Battery SOC sensor for threshold logic
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/home_battery.yaml
input:
sensors:
name: Tibber Prices Sensors
icon: mdi:chart-timeline-variant-shimmer
description: Select the period sensors from Tibber Prices.
input:
best_price_sensor:
name: Best Price Period Sensor
description: >
`binary_sensor.<home>_best_price_period` — triggers
charging.
selector:
entity:
filter:
domain: binary_sensor
integration: tibber_prices
peak_price_sensor:
name: Peak Price Period Sensor
description: >
`binary_sensor.<home>_peak_price_period` — triggers
discharging.
selector:
entity:
filter:
domain: binary_sensor
integration: tibber_prices
battery:
name: Battery
icon: mdi:battery-charging-60
description: Configure your battery switches and thresholds.
input:
charge_switch:
name: Grid Charging Switch
description: >
Switch that enables charging from the grid.
selector:
entity:
filter:
domain: switch
discharge_switch:
name: Grid Discharge Switch
description: >
Switch that enables discharging to grid / home.
selector:
entity:
filter:
domain: switch
soc_sensor:
name: Battery SOC Sensor (optional)
description: >
State of Charge sensor (0100%). Leave empty to skip
SOC checks.
default: ""
selector:
entity:
filter:
domain: sensor
device_class: battery
charge_max_soc:
name: Max SOC for Charging
description: >
Only charge from grid if SOC is below this level.
default: 90
selector:
number:
min: 50
max: 100
step: 5
unit_of_measurement: "%"
discharge_min_soc:
name: Min SOC for Discharging
description: >
Only discharge if SOC is above this level.
default: 20
selector:
number:
min: 5
max: 50
step: 5
unit_of_measurement: "%"
check_volatility:
name: Skip Charging on Flat-Price Days
description: >
When enabled, grid charging is skipped when volatility
is "low" (charging from grid wouldn't save much money).
default: true
selector:
boolean:
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect helpers to override settings from your
dashboard at runtime. When a helper is connected and has
a valid value, it takes priority over the fixed default.
Leave empty to always use the fixed defaults.
input:
charge_max_soc_override:
name: "Override: Max SOC for Charging"
description: >
`input_number` helper to adjust the charge threshold
from your dashboard (e.g., before travel or bad weather).
**Create in Settings → Helpers → Number**
(min: 50, max: 100, step: 5, unit: %).
default: ""
selector:
entity:
filter:
domain: input_number
discharge_min_soc_override:
name: "Override: Min SOC for Discharging"
description: >
`input_number` helper to adjust the discharge threshold
from your dashboard.
**Create in Settings → Helpers → Number**
(min: 5, max: 50, step: 5, unit: %).
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional mobile notifications for charge/discharge events.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
mode: restart
triggers:
- trigger: state
entity_id: !input best_price_sensor
to: "on"
id: charge_start
- trigger: state
entity_id: !input best_price_sensor
to: "off"
id: charge_end
- trigger: state
entity_id: !input peak_price_sensor
to: "on"
id: discharge_start
- trigger: state
entity_id: !input peak_price_sensor
to: "off"
id: discharge_end
variables:
_blueprint_variant: "home_battery"
best_price_sensor: !input best_price_sensor
peak_price_sensor: !input peak_price_sensor
charge_switch: !input charge_switch
discharge_switch: !input discharge_switch
soc_sensor: !input soc_sensor
_charge_max_soc_default: !input charge_max_soc
_charge_max_soc_override: !input charge_max_soc_override
charge_max_soc: >
{% set o = _charge_max_soc_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | int(_charge_max_soc_default) }}
{% else %}
{{ _charge_max_soc_default }}
{% endif %}
_discharge_min_soc_default: !input discharge_min_soc
_discharge_min_soc_override: !input discharge_min_soc_override
discharge_min_soc: >
{% set o = _discharge_min_soc_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | int(_discharge_min_soc_default) }}
{% else %}
{{ _discharge_min_soc_default }}
{% endif %}
check_volatility: !input check_volatility
notify_service: !input notify_service
actions:
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- stop: "Tibber Prices integration not found"
# ════════════════════════════════════════════════════════
# CHARGE / DISCHARGE / STOP
# ════════════════════════════════════════════════════════
- choose:
# ── CHARGE during Best Price Period ──
- conditions:
- condition: trigger
id: charge_start
sequence:
# Volatility check
- if:
- condition: template
value_template: >
{{ check_volatility
and state_attr(best_price_sensor, 'volatility')
| default('normal') == 'low' }}
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔋 Battery — Skipped (Low Volatility)"
message: >
Prices are flat today. Grid charging skipped
(savings would be minimal).
- stop: "Low volatility — skipping grid charge"
# SOC check
- if:
- condition: template
value_template: >
{{ soc_sensor | length > 0
and states(soc_sensor) | int(0) >= charge_max_soc | int }}
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔋 Battery — Already Charged"
message: >
SOC at {{ states(soc_sensor) }}% (max:
{{ charge_max_soc }}%). Skipping.
- stop: "SOC above charge threshold"
# Start charging
- action: switch.turn_on
target:
entity_id: "{{ charge_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔋 Battery — Grid Charging"
message: >
Best price period started. Charging from grid.
{% if soc_sensor | length > 0 %}
SOC: {{ states(soc_sensor) }}%.
{% endif %}
# ── DISCHARGE during Peak Price Period ──
- conditions:
- condition: trigger
id: discharge_start
sequence:
# SOC check
- if:
- condition: template
value_template: >
{{ soc_sensor | length > 0
and states(soc_sensor) | int(0) <= discharge_min_soc | int }}
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔋 Battery — Too Low to Discharge"
message: >
SOC at {{ states(soc_sensor) }}% (min:
{{ discharge_min_soc }}%). Skipping.
- stop: "SOC below discharge threshold"
# Start discharging
- action: switch.turn_on
target:
entity_id: "{{ discharge_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔋 Battery — Discharging"
message: >
Peak price period started. Discharging battery.
{% if soc_sensor | length > 0 %}
SOC: {{ states(soc_sensor) }}%.
{% endif %}
# ── STOP charging when best price ends ──
- conditions:
- condition: trigger
id: charge_end
sequence:
- action: switch.turn_off
target:
entity_id: "{{ charge_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔋 Battery — Charge Stopped"
message: Best price period ended. Grid charging off.
# ── STOP discharging when peak price ends ──
- conditions:
- condition: trigger
id: discharge_end
sequence:
- action: switch.turn_off
target:
entity_id: "{{ discharge_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔋 Battery — Discharge Stopped"
message: Peak price period ended. Grid discharge off.

View file

@ -0,0 +1,588 @@
blueprint:
name: "Tibber Prices: Laundry Day Pipeline (Smart Plug)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Schedule multiple wash + dry cycles at the cheapest electricity prices
using smart plug switches.
Open your
[Tibber Prices configuration](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)
to verify the integration is installed and set up.
**What it does:**
- Plans 15 wash + dry cycles with automatic price optimization
- Finds the cheapest time windows for each appliance cycle
- Sends mobile notifications for laundry transfer reminders
- Optional pipeline mode: next wash starts while dryer runs
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- Two helpers (created in Settings → Helpers):
- Toggle (`input_boolean`) — starts laundry day when turned on
- Number (`input_number`, min 1, max 5, step 1) — how many loads
- Smart plug switches for washer and dryer
**How it works:**
```
Load 1: [══ Wash 1 ══] → transfer → [══ Dry 1 ══]
Load 2: (pipeline) [══ Wash 2 ══] → transfer → [══ Dry 2 ══]
```
1. Turn on the toggle to start laundry day
2. Each wash + dry cycle is planned at the cheapest available price
3. You receive notifications when it's time to transfer laundry
4. The toggle turns off automatically when all loads are done
**Pipeline mode** (optional): When your wash cycle takes longer than
your dry cycle, the next wash can start while the dryer is still
running. This significantly reduces total laundry time.
**Other variants:**
[Home Connect](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline_home_connect.yaml)
·
[Home Connect Alt](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline_home_connect_alt.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline.yaml
input:
appliances:
name: Appliances
icon: mdi:washing-machine
description: Configure your washing machine and dryer.
input:
washer_switch:
name: Washing Machine Switch
description: Smart plug controlling the washing machine.
selector:
entity:
filter:
domain: switch
include_dryer:
name: Include Dryer
description: >
Enable to schedule dryer cycles after each wash.
Disable if you hang laundry to dry.
default: true
selector:
boolean:
dryer_switch:
name: Dryer Switch
description: >
Smart plug controlling the dryer.
Only used when "Include Dryer" is enabled.
selector:
entity:
filter:
domain: switch
durations:
name: Program Durations
icon: mdi:timer-outline
description: >
Set typical program durations for your appliances.
Include a small buffer (~5 min) for cycle-to-cycle variation.
input:
washer_duration:
name: Wash Cycle Duration
description: >
Typical wash program duration in minutes.
ECO 40-60 ≈ 90 min, Cotton 60°C ≈ 120 min, Quick ≈ 45 min.
default: 95
selector:
number:
min: 15
max: 240
step: 5
unit_of_measurement: min
mode: slider
dryer_duration:
name: Dry Cycle Duration
description: >
Typical dry program duration in minutes.
Cotton Dry ≈ 60 min, Extra Dry ≈ 75 min, Gentle ≈ 90 min.
default: 65
selector:
number:
min: 15
max: 180
step: 5
unit_of_measurement: min
mode: slider
transfer_time:
name: Transfer Time
description: >
Minutes to transfer laundry from washer to dryer.
You'll get a notification when it's time.
default: 15
selector:
number:
min: 5
max: 60
step: 5
unit_of_measurement: min
mode: slider
schedule:
name: Schedule
icon: mdi:calendar-clock
description: Configure the trigger, load count, and deadline.
input:
trigger_entity:
name: Laundry Day Toggle
description: >
An `input_boolean` helper that starts laundry day when turned on.
Create in Settings → Helpers → Toggle.
selector:
entity:
filter:
domain: input_boolean
loads_entity:
name: Number of Loads
description: >
An `input_number` helper (15) for how many wash cycles to run.
Create in Settings → Helpers → Number (min: 1, max: 5, step: 1).
selector:
entity:
filter:
domain: input_number
deadline_time:
name: Must Finish By
description: >
All laundry must be finished by this time today.
The scheduler only looks for cheap windows before this deadline.
default: "22:00:00"
selector:
time:
advanced:
name: Advanced
icon: mdi:cog
collapsed: true
description: Pipeline mode and fine-tuning options.
input:
pipeline_mode:
name: Pipeline Mode
description: >
When enabled, the next wash starts immediately after the dryer
begins — without waiting for the dryer to finish. This creates
a pipeline where washer and dryer overlap, cutting total time
by roughly one dry cycle per load.
**Only safe when wash duration ≥ dryer duration.**
If your dryer takes longer than your washer, leave this off.
default: false
selector:
boolean:
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect helpers to override durations from your
dashboard at runtime. When a helper is connected and has
a valid value, it takes priority over the fixed default.
Leave empty to always use the fixed defaults.
input:
washer_duration_override:
name: "Override: Wash Cycle Duration"
description: >
`input_number` helper to change the wash duration from
your dashboard (e.g., ECO vs. Quick program).
**Create in Settings → Helpers → Number**
(min: 15, max: 240, step: 5, unit: min).
default: ""
selector:
entity:
filter:
domain: input_number
dryer_duration_override:
name: "Override: Dry Cycle Duration"
description: >
`input_number` helper to change the dry duration from
your dashboard.
**Create in Settings → Helpers → Number**
(min: 15, max: 180, step: 5, unit: min).
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: Optional mobile notifications for transfer reminders and progress.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
# Only one laundry day at a time
mode: single
max_exceeded: warning
triggers:
- trigger: state
entity_id: !input trigger_entity
to: "on"
# Expose inputs as template variables
variables:
# Blueprint versioning — for compatibility checks
_blueprint_variant: "smart_plug"
# Input variables
washer_switch: !input washer_switch
dryer_switch: !input dryer_switch
include_dryer: !input include_dryer
_washer_duration_default: !input washer_duration
_washer_duration_override: !input washer_duration_override
washer_duration: >
{% set o = _washer_duration_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | int(_washer_duration_default) }}
{% else %}
{{ _washer_duration_default }}
{% endif %}
_dryer_duration_default: !input dryer_duration
_dryer_duration_override: !input dryer_duration_override
dryer_duration: >
{% set o = _dryer_duration_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | int(_dryer_duration_default) }}
{% else %}
{{ _dryer_duration_default }}
{% endif %}
transfer_time: !input transfer_time
loads_entity: !input loads_entity
deadline_time: !input deadline_time
pipeline_mode: !input pipeline_mode
notify_service: !input notify_service
total_loads: "{{ states(loads_entity) | int(1) }}"
trigger_entity: !input trigger_entity
actions:
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
# Check: Integration installed?
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🧺 Laundry Day — Setup Required"
message: >
The Tibber Prices integration is not installed or not
configured. Install it via HACS and set up your Tibber
account before using this blueprint.
- action: input_boolean.turn_off
target:
entity_id: "{{ trigger_entity }}"
- stop: "Tibber Prices integration not found"
# ════════════════════════════════════════════════════════
# VALIDATION
# ════════════════════════════════════════════════════════
- if:
- condition: template
value_template: "{{ total_loads < 1 or total_loads > 5 }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🧺 Laundry Day"
message: >
Invalid number of loads: {{ total_loads }}.
Set {{ loads_entity }} between 1 and 5.
- action: input_boolean.turn_off
target:
entity_id: "{{ trigger_entity }}"
- stop: "Invalid load count"
# ════════════════════════════════════════════════════════
# START NOTIFICATION
# ════════════════════════════════════════════════════════
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🧺 Laundry Day Started"
message: >
Planning {{ total_loads }}
load{{ 's' if total_loads | int > 1 else '' }}
(wash {{ washer_duration }} min
{{ '+ dry ' ~ dryer_duration ~ ' min' if include_dryer else '' }}).
Must finish by {{ deadline_time[:5] }}.
# ════════════════════════════════════════════════════════
# MAIN PIPELINE LOOP
# ════════════════════════════════════════════════════════
- repeat:
count: "{{ total_loads }}"
sequence:
# Check if user cancelled (turned off the toggle)
- if:
- condition: template
value_template: "{{ is_state(trigger_entity, 'off') }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🧺 Laundry Day Cancelled"
message: >
Stopped after {{ repeat.index - 1 }}
of {{ total_loads }} loads.
- stop: "Cancelled by user"
# ── PLAN WASH ──────────────────────────────────
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(washer_duration | int) // 60,
(washer_duration | int) % 60) }}
must_finish_by: >
{{ today_at(deadline_time).isoformat() }}
response_variable: wash_result
- if:
- condition: template
value_template: "{{ not wash_result.window_found }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🧺 Laundry Day — Problem"
message: >
No cheap window found for wash {{ repeat.index }}/{{ total_loads }}.
{{ wash_result.reason | default('Not enough time before deadline?') }}
- action: input_boolean.turn_off
target:
entity_id: "{{ trigger_entity }}"
- stop: "No wash window found"
# ── WAIT UNTIL WASH START ──────────────────────
- delay:
seconds: >
{{ max(0,
((wash_result.window.start | as_datetime) - now())
.total_seconds() | int) }}
# ── START WASH ─────────────────────────────────
- action: switch.turn_on
target:
entity_id: "{{ washer_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: >
👕 Wash {{ repeat.index }}/{{ total_loads }} Started
message: >
Running until
~{{ (now() + timedelta(minutes=washer_duration | int))
| as_timestamp | timestamp_custom('%H:%M') }}.
Price: {{ wash_result.window.price_mean | round(1) }}
{{ wash_result.price_unit }}/kWh avg.
{% if wash_result.relaxation_applied | default(false) %}
(Filters relaxed to find window.)
{% endif %}
# ── WAIT FOR WASH TO COMPLETE ──────────────────
- delay:
minutes: "{{ washer_duration }}"
# ── WASH DONE ─────────────────────────────────
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: >
✅ Wash {{ repeat.index }}/{{ total_loads }} Done!
message: >
{% if include_dryer %}
Transfer laundry to the dryer!
{% endif %}
{% if repeat.index | int < total_loads | int %}
{% if include_dryer %}Then load{% else %}Load{% endif %}
the washer for load {{ repeat.index + 1 }}.
{% endif %}
# ── DRYER (if enabled) ─────────────────────────
- if:
- condition: template
value_template: "{{ include_dryer }}"
then:
# Wait for transfer
- delay:
minutes: "{{ transfer_time }}"
# Plan dryer
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(dryer_duration | int) // 60,
(dryer_duration | int) % 60) }}
must_finish_by: >
{{ today_at(deadline_time).isoformat() }}
response_variable: dry_result
- if:
- condition: template
value_template: "{{ not dry_result.window_found }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "⚠️ Dryer {{ repeat.index }} — No Window"
message: >
No cheap window found for dryer {{ repeat.index }}.
Consider running the dryer manually.
{{ dry_result.reason | default('') }}
# Don't abort — continue with next wash cycle
- if:
- condition: template
value_template: >
{{ dry_result.window_found | default(false) }}
then:
# Wait until dryer start
- delay:
seconds: >
{{ max(0,
((dry_result.window.start | as_datetime) - now())
.total_seconds() | int) }}
# START DRYER
- action: switch.turn_on
target:
entity_id: "{{ dryer_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: >
🌀 Dryer {{ repeat.index }}/{{ total_loads }}
Started
message: >
Running until
~{{ (now() + timedelta(minutes=dryer_duration | int))
| as_timestamp | timestamp_custom('%H:%M') }}.
{% if pipeline_mode
and repeat.index | int < total_loads | int %}
Next wash will be planned now —
dryer runs in parallel.
{% endif %}
# Wait for dryer to finish
# UNLESS pipeline mode AND more loads to come
- if:
- condition: template
value_template: >
{{ not (pipeline_mode
and repeat.index | int < total_loads | int) }}
then:
- delay:
minutes: "{{ dryer_duration }}"
# ════════════════════════════════════════════════════════
# ALL DONE
# ════════════════════════════════════════════════════════
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🎉 Laundry Day Complete!"
message: >
All {{ total_loads }}
load{{ 's' if total_loads | int > 1 else '' }}
washed{{ ' and dried' if include_dryer else '' }}.
Time to fold! 🧺
- action: input_boolean.turn_off
target:
entity_id: "{{ trigger_entity }}"

View file

@ -0,0 +1,284 @@
blueprint:
name: "Tibber Prices: Washing Machine (Smart Plug)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Automatically run your washing machine at the cheapest electricity
price overnight using a smart plug.
Open your
[Tibber Prices configuration](https://my.home-assistant.io/redirect/integration/?domain=tibber_prices)
to verify the integration is installed and set up.
**What it does:**
- Plans the cheapest window overnight for one wash cycle
- Starts the washing machine automatically at the cheapest time
- Sends a notification with the planned time and price
- Survives Home Assistant restarts (uses `input_datetime` helper)
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- One helper (created in Settings → Helpers):
- Date & Time (`input_datetime`) — stores the planned start time
- Smart plug switch for the washing machine
**Tip:** For multiple wash + dry cycles in one day, use the
[Laundry Day Pipeline](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline.yaml)
blueprint instead.
**Other variants:**
[Home Connect](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine_home_connect.yaml)
·
[Home Connect Alt](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine_home_connect_alt.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine.yaml
input:
appliance:
name: Appliance
icon: mdi:washing-machine
description: Select the smart plug that controls your washing machine.
input:
appliance_switch:
name: Washing Machine Smart Plug
description: The switch entity controlling the washing machine.
selector:
entity:
filter:
domain: switch
schedule:
name: Schedule
icon: mdi:calendar-clock
description: Configure when to plan and the search window.
input:
plan_time:
name: Planning Time
description: >
When to search for the cheapest window each day.
default: "20:00:00"
selector:
time:
start_helper:
name: Start Time Helper
description: >
An `input_datetime` helper (type: Date and Time) that stores
the planned start time. Create in Settings → Helpers.
selector:
entity:
filter:
domain: input_datetime
duration:
name: Program Duration
description: >
Typical wash program duration in minutes.
ECO 40-60 ≈ 90 min, Cotton 60°C ≈ 120 min, Quick ≈ 45 min.
default: 95
selector:
number:
min: 15
max: 240
step: 5
unit_of_measurement: min
mode: slider
search_start:
name: Search Window Start
description: >
Earliest time the washing machine may start.
Typically late evening after loading.
default: "22:00:00"
selector:
time:
search_end:
name: Search Window End
description: >
Latest time the wash must finish by.
The program must complete before this time.
default: "06:00:00"
selector:
time:
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect helpers to override settings from your
dashboard at runtime. When a helper is connected and has
a valid value, it takes priority over the fixed default.
Leave empty to always use the fixed defaults.
input:
duration_override:
name: "Override: Program Duration"
description: >
`input_number` helper to change the duration from your
dashboard without reconfiguring the blueprint.
**Create in Settings → Helpers → Number** with the same
min/max as the Duration slider above.
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: Optional mobile notifications.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: time
at: !input plan_time
id: plan
- trigger: time
at: !input start_helper
id: execute
variables:
_blueprint_variant: "smart_plug"
appliance_switch: !input appliance_switch
start_helper: !input start_helper
_duration_default: !input duration
_duration_override: !input duration_override
duration: >
{% set o = _duration_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | int(_duration_default) }}
{% else %}
{{ _duration_default }}
{% endif %}
search_start: !input search_start
search_end: !input search_end
notify_service: !input notify_service
actions:
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "👕 Washing Machine — Setup Required"
message: >
The Tibber Prices integration is not installed or not
configured. Install it via HACS and set up your Tibber
account before using this blueprint.
- stop: "Tibber Prices integration not found"
# ════════════════════════════════════════════════════════
# PLAN / EXECUTE
# ════════════════════════════════════════════════════════
- choose:
- conditions:
- condition: trigger
id: plan
sequence:
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
search_start_time: "{{ search_start }}"
search_end_time: "{{ search_end }}"
search_end_day_offset: 1
response_variable: result
- if:
- condition: template
value_template: "{{ result.window_found }}"
then:
- action: input_datetime.set_datetime
target:
entity_id: "{{ start_helper }}"
data:
datetime: "{{ result.window.start }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "👕 Washing Machine Planned"
message: >
Start at {{ result.window.start | as_datetime
| as_local | as_timestamp
| timestamp_custom('%H:%M') }}.
Avg price: {{ result.window.price_mean | round(1) }}
{{ result.price_unit }}/kWh.
{% if result.relaxation_applied | default(false) %}
(Filters relaxed to find window.)
{% endif %}
else:
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "👕 Washing Machine"
message: >
No cheap window found. Consider running manually
or adjusting the search window.
- conditions:
- condition: trigger
id: execute
sequence:
- action: switch.turn_on
target:
entity_id: "{{ appliance_switch }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "👕 Washing Machine Started"
message: >
Smart plug turned on. Program should finish in
~{{ duration }} minutes.

View file

@ -0,0 +1,458 @@
blueprint:
name: "Tibber Prices: Washing Machine (Home Connect)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v2.0.0
**Device-driven** washing machine automation with electricity price
optimization using the **Home Connect** integration (HA Core).
**How it works:**
1. Select your program on the washing machine
2. Close the door and enable Remote Start
3. The blueprint reads the estimated duration from the device
4. Finds the cheapest electricity window before your deadline
5. Tells the machine when to finish via `FinishInRelative`
6. The machine calculates when to start and manages the countdown
internally — no HA timers
**Important:** Washing machines use `FinishInRelative` (not
`StartInRelative` like dishwashers). The appliance receives the
deadline and calculates the optimal start time itself.
**No scheduling needed** — the machine handles the delayed start
itself. No `input_datetime` helpers required. Survives HA restarts
because the countdown runs on the appliance.
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- [Home Connect](https://www.home-assistant.io/integrations/home_connect/) integration configured
- **Remote Start** enabled on the washing machine
**Tip:** For multiple wash + dry cycles, use the
[Laundry Day Pipeline](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline_home_connect.yaml)
blueprint instead.
**Other variants:**
[Smart Plug](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine.yaml)
·
[Home Connect Alt](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine_home_connect_alt.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.11.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine_home_connect.yaml
input:
appliance:
name: Appliance
icon: mdi:washing-machine
description: >
Select your Home Connect washing machine device and entities.
input:
appliance_device:
name: Washing Machine Device
description: >
Your washing machine from the Home Connect integration.
Used to target the start command.
selector:
device:
filter:
integration: home_connect
door_sensor:
name: Door Sensor
description: >
The door sensor of your washing machine.
selector:
entity:
filter:
integration: home_connect
domain: binary_sensor
device_class: door
remote_start_sensor:
name: Remote Control Sensor
description: >
The "Remote Control Active" binary sensor.
Must be **on** for the automation to proceed.
selector:
entity:
filter:
integration: home_connect
domain: binary_sensor
estimated_duration_entity:
name: Estimated Program Duration
description: >
The "Estimated Total Program Time" sensor.
If unavailable, the fallback duration is used instead.
selector:
entity:
filter:
integration: home_connect
domain: sensor
operation_state_entity:
name: Operation State
description: >
The "Operation State" sensor.
Used to verify the machine is ready before planning.
selector:
entity:
filter:
integration: home_connect
domain: sensor
schedule:
name: Schedule
icon: mdi:calendar-clock
description: >
Configure the deadline and fallback duration.
input:
must_finish_by:
name: Must Finish By
description: >
The program must be finished by this time.
If this time has already passed today, the deadline
automatically moves to tomorrow (overnight mode).
default: "06:00:00"
selector:
time:
duration_fallback:
name: Fallback Duration (minutes)
description: >
Used **only** if the device doesn't report the estimated
duration. Normally the duration is read automatically.
ECO 40-60 ≈ 90 min, Cotton 60°C ≈ 120 min, Quick ≈ 45 min.
default: 95
selector:
number:
min: 15
max: 240
step: 5
unit_of_measurement: min
mode: slider
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional notifications. Use **simple mode** (just a service)
or point to an **advanced script** for multi-target,
presence-aware, and platform-specific notifications.
input:
notify_service:
name: Quick Notification (Simple)
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Ignored when the advanced script is set.
default: ""
selector:
text:
notification_script:
name: Notification Script (Advanced)
description: >
A `script.*` entity for advanced notifications
(multiple recipients, presence filtering, iOS/Android).
When set, replaces the simple notification.
Receives structured variables (event_type, appliance,
title, message, and context data).
default: ""
selector:
entity:
filter:
domain: script
title_setup_required:
name: "Title: Setup Required"
default: "👕 Washing Machine — Setup Required"
selector:
text:
title_not_ready:
name: "Title: Not Ready"
default: "👕 Washing Machine — Not Ready"
selector:
text:
title_no_cheap_slot:
name: "Title: No Cheap Slot"
default: "👕 Washing Machine — No Cheap Slot"
selector:
text:
title_planned:
name: "Title: Planned"
default: "👕 Washing Machine — Planned!"
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: state
entity_id: !input door_sensor
to: "off"
- trigger: state
entity_id: !input remote_start_sensor
to: "on"
conditions:
- condition: state
entity_id: !input door_sensor
state: "off"
- condition: state
entity_id: !input remote_start_sensor
state: "on"
variables:
_blueprint_variant: "home_connect"
appliance_device: !input appliance_device
door_sensor: !input door_sensor
remote_start_sensor: !input remote_start_sensor
estimated_duration_entity: !input estimated_duration_entity
operation_state_entity: !input operation_state_entity
must_finish_by_time: !input must_finish_by
duration_fallback: !input duration_fallback
notify_service: !input notify_service
notification_script: !input notification_script
title_setup_required: !input title_setup_required
title_not_ready: !input title_not_ready
title_no_cheap_slot: !input title_no_cheap_slot
title_planned: !input title_planned
actions:
# ════════════════════════════════════════════════════════
# PREFLIGHT CHECKS
# ════════════════════════════════════════════════════════
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- variables:
_n_title: "{{ title_setup_required }}"
_n_message: >
Install the Tibber Prices integration via HACS and
configure your Tibber account.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: setup_required
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Tibber Prices integration not found"
- if:
- condition: template
value_template: >
{% set op = states(operation_state_entity) %}
{{ op not in ['unknown', 'unavailable']
and 'Ready' not in op
and 'Inactive' not in op }}
then:
- variables:
_n_title: "{{ title_not_ready }}"
_n_message: >
State: {{ states(operation_state_entity) }}.
Ensure it's idle with Remote Start enabled.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: not_ready
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Machine not ready"
# ════════════════════════════════════════════════════════
# READ DEVICE DATA
# ════════════════════════════════════════════════════════
- variables:
_raw_duration: "{{ states(estimated_duration_entity) }}"
duration: >
{% set raw = states(estimated_duration_entity) %}
{% if raw not in ['unknown', 'unavailable', 'None', '']
and ':' in raw %}
{% set parts = raw.split(':') %}
{{ (parts[0] | int(0)) * 60 + (parts[1] | int(0)) }}
{% elif raw not in ['unknown', 'unavailable', 'None', '']
and raw | int(0) > 0 %}
{{ raw | int }}
{% else %}
{{ duration_fallback }}
{% endif %}
deadline: >
{% set dl = today_at(must_finish_by_time) %}
{% if dl <= now() %}
{{ (dl + timedelta(days=1)).isoformat() }}
{% else %}
{{ dl.isoformat() }}
{% endif %}
# ════════════════════════════════════════════════════════
# FIND CHEAPEST WINDOW
# ════════════════════════════════════════════════════════
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
must_finish_by: "{{ deadline }}"
response_variable: result
- if:
- condition: template
value_template: "{{ not result.window_found }}"
then:
- variables:
_n_title: "{{ title_no_cheap_slot }}"
_n_message: >
No cheap slot before
{{ deadline | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
for {{ duration }} min.
Run manually or extend the deadline.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_window
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
deadline: "{{ deadline }}"
duration_minutes: "{{ duration | int }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No cheap window found"
# ════════════════════════════════════════════════════════
# START WITH DELAY (device manages countdown)
# ════════════════════════════════════════════════════════
- variables:
_window_start: "{{ result.window.start | as_datetime }}"
_window_end: >
{{ (_window_start + timedelta(minutes=duration | int)).isoformat() }}
finish_in_relative: >
{% set window_end = _window_start + timedelta(minutes=duration | int) %}
{% set seconds_until_end = ((window_end - now()).total_seconds()) | int %}
{{ [duration | int * 60, seconds_until_end] | max }}
# Washing machines use FinishInRelative
- action: home_connect.set_program_and_options
target:
device_id: "{{ appliance_device }}"
data:
affects_to: active_program
b_s_h_common_option_finish_in_relative: "{{ finish_in_relative }}"
- variables:
_n_title: "{{ title_planned }}"
_n_message: >
{% set delay = finish_in_relative | int - (duration | int * 60) %}
{% if delay > 0 %}
⏰ ~{{ _window_start | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
(in {{ (delay / 3600) | round(1) }} h)
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% else %}
▶️ Starting now!
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% endif %}
{% if _raw_duration in ['unknown', 'unavailable', 'None', ''] %}
· ⚠️ Duration estimated
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: planned
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
start_time: "{{ _window_start | as_local | as_timestamp | timestamp_custom('%Y-%m-%dT%H:%M:%S') }}"
duration_minutes: "{{ duration | int }}"
price_mean: "{{ result.window.price_mean | round(1) }}"
price_unit: "{{ result.price_unit }}"
using_fallback_duration: "{{ _raw_duration in ['unknown', 'unavailable', 'None', ''] }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"

View file

@ -0,0 +1,513 @@
blueprint:
name: "Tibber Prices: Washing Machine (Home Connect Alt)"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v2.0.0
**Device-driven** washing machine automation with electricity price
optimization using **Home Connect Alt**
([HACS integration by ekutner](https://my.home-assistant.io/redirect/hacs_repository/?owner=ekutner&repository=home-connect-hass&category=integration)).
**How it works:**
1. Select your program on the washing machine
2. Close the door and enable Remote Start
3. The blueprint reads the program and estimated duration from the
device automatically
4. Finds the cheapest electricity window before your deadline
5. Tells the washing machine when to finish via `FinishInRelative`
6. The machine calculates when to start and manages the countdown
internally — no HA timers
**Important:** Washing machines use `FinishInRelative` (not
`StartInRelative` like dishwashers). The appliance receives the
deadline and calculates the optimal start time itself.
**No scheduling needed** — the machine handles the delayed start
itself. No `input_datetime` helpers required. Survives HA restarts
because the countdown runs on the appliance.
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- [Home Connect Alt](https://my.home-assistant.io/redirect/hacs_repository/?owner=ekutner&repository=home-connect-hass&category=integration) integration configured
- **Remote Start** enabled on the washing machine
**Tip:** For multiple wash + dry cycles, use the
[Laundry Day Pipeline](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/laundry_day_pipeline_home_connect_alt.yaml)
blueprint instead.
**Other variants:**
[Smart Plug](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine.yaml)
·
[Home Connect](https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine_home_connect.yaml)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.11.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/washing_machine_home_connect_alt.yaml
input:
appliance:
name: Appliance Entities
icon: mdi:washing-machine
description: >
Select your Home Connect Alt washing machine entities.
All entities belong to the same appliance device.
input:
program_entity:
name: Program Select Entity
description: >
The **Programs** select entity of your washing machine
(e.g., `select.washer_programs`).
Used to read the selected program and as target for starting.
selector:
entity:
filter:
integration: home_connect_alt
domain: select
door_sensor:
name: Door Sensor
description: >
The door sensor of your washing machine
(e.g., `binary_sensor.washer_door`).
selector:
entity:
filter:
integration: home_connect_alt
domain: binary_sensor
device_class: door
remote_start_sensor:
name: Remote Control Sensor
description: >
The "Remote Control Active" binary sensor
(e.g., `binary_sensor.washer_remote_control_active`).
Must be **on** for the automation to proceed.
selector:
entity:
filter:
integration: home_connect_alt
domain: binary_sensor
estimated_duration_entity:
name: Estimated Program Duration
description: >
The "Estimated Total Program Time" sensor
(e.g., `sensor.washer_estimated_total_program_time`).
Shows the expected duration in `H:MM` format.
If unavailable, the fallback duration is used instead.
selector:
entity:
filter:
integration: home_connect_alt
domain: sensor
operation_state_entity:
name: Operation State
description: >
The "Operation State" sensor
(e.g., `sensor.washer_operation_state`).
Used to verify the machine is ready before planning.
selector:
entity:
filter:
integration: home_connect_alt
domain: sensor
schedule:
name: Schedule
icon: mdi:calendar-clock
description: >
Configure the deadline and fallback duration.
input:
must_finish_by:
name: Must Finish By
description: >
The program must be finished by this time.
If this time has already passed today, the deadline
automatically moves to tomorrow (overnight mode).
default: "06:00:00"
selector:
time:
duration_fallback:
name: Fallback Duration (minutes)
description: >
Used **only** if the device doesn't report the estimated
duration (e.g., program not yet fully selected on the
appliance). Normally the duration is read automatically.
ECO 40-60 ≈ 90 min, Cotton 60°C ≈ 120 min, Quick ≈ 45 min.
default: 95
selector:
number:
min: 15
max: 240
step: 5
unit_of_measurement: min
mode: slider
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional notifications. Use **simple mode** (just a service)
or point to an **advanced script** for multi-target,
presence-aware, and platform-specific notifications.
input:
notify_service:
name: Quick Notification (Simple)
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Ignored when the advanced script is set.
default: ""
selector:
text:
notification_script:
name: Notification Script (Advanced)
description: >
A `script.*` entity for advanced notifications
(multiple recipients, presence filtering, iOS/Android).
When set, replaces the simple notification.
Receives structured variables (event_type, appliance,
title, message, and context data).
default: ""
selector:
entity:
filter:
domain: script
title_setup_required:
name: "Title: Setup Required"
default: "👕 Washing Machine — Setup Required"
selector:
text:
title_not_ready:
name: "Title: Not Ready"
default: "👕 Washing Machine — Not Ready"
selector:
text:
title_no_program:
name: "Title: No Program"
default: "👕 Washing Machine — No Program"
selector:
text:
title_no_cheap_slot:
name: "Title: No Cheap Slot"
default: "👕 Washing Machine — No Cheap Slot"
selector:
text:
title_planned:
name: "Title: Planned"
default: "👕 Washing Machine — Planned!"
selector:
text:
mode: single
max_exceeded: silent
triggers:
- trigger: state
entity_id: !input door_sensor
to: "off"
- trigger: state
entity_id: !input remote_start_sensor
to: "on"
conditions:
- condition: state
entity_id: !input door_sensor
state: "off"
- condition: state
entity_id: !input remote_start_sensor
state: "on"
variables:
_blueprint_variant: "home_connect_alt"
program_entity: !input program_entity
door_sensor: !input door_sensor
remote_start_sensor: !input remote_start_sensor
estimated_duration_entity: !input estimated_duration_entity
operation_state_entity: !input operation_state_entity
must_finish_by_time: !input must_finish_by
duration_fallback: !input duration_fallback
notify_service: !input notify_service
notification_script: !input notification_script
title_setup_required: !input title_setup_required
title_not_ready: !input title_not_ready
title_no_program: !input title_no_program
title_no_cheap_slot: !input title_no_cheap_slot
title_planned: !input title_planned
actions:
# ════════════════════════════════════════════════════════
# PREFLIGHT CHECKS
# ════════════════════════════════════════════════════════
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- variables:
_n_title: "{{ title_setup_required }}"
_n_message: >
Install the Tibber Prices integration via HACS and
configure your Tibber account.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: setup_required
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Tibber Prices integration not found"
- if:
- condition: template
value_template: >
{% set op = states(operation_state_entity) %}
{{ op not in ['unknown', 'unavailable']
and 'Ready' not in op
and 'Inactive' not in op }}
then:
- variables:
_n_title: "{{ title_not_ready }}"
_n_message: >
State: {{ states(operation_state_entity) }}.
Ensure it's idle with Remote Start enabled.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: not_ready
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "Machine not ready"
# ════════════════════════════════════════════════════════
# READ DEVICE DATA
# ════════════════════════════════════════════════════════
- variables:
selected_program: "{{ states(program_entity) }}"
_raw_duration: "{{ states(estimated_duration_entity) }}"
duration: >
{% set raw = states(estimated_duration_entity) %}
{% if raw not in ['unknown', 'unavailable', 'None', '']
and ':' in raw %}
{% set parts = raw.split(':') %}
{{ (parts[0] | int(0)) * 60 + (parts[1] | int(0)) }}
{% else %}
{{ duration_fallback }}
{% endif %}
deadline: >
{% set dl = today_at(must_finish_by_time) %}
{% if dl <= now() %}
{{ (dl + timedelta(days=1)).isoformat() }}
{% else %}
{{ dl.isoformat() }}
{% endif %}
- if:
- condition: template
value_template: >
{{ selected_program in ['unknown', 'unavailable', 'None', ''] }}
then:
- variables:
_n_title: "{{ title_no_program }}"
_n_message: >
Select a program, close the door, and enable
Remote Start.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_program
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No program selected"
# ════════════════════════════════════════════════════════
# FIND CHEAPEST WINDOW
# ════════════════════════════════════════════════════════
- action: tibber_prices.find_cheapest_block
data:
duration: >
{{ '%02d:%02d:00' | format(
(duration | int) // 60,
(duration | int) % 60) }}
must_finish_by: "{{ deadline }}"
response_variable: result
- if:
- condition: template
value_template: "{{ not result.window_found }}"
then:
- variables:
_n_title: "{{ title_no_cheap_slot }}"
_n_message: >
No cheap slot before
{{ deadline | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
for {{ duration }} min.
Run manually or extend the deadline.
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: no_window
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
deadline: "{{ deadline }}"
duration_minutes: "{{ duration | int }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"
- stop: "No cheap window found"
# ════════════════════════════════════════════════════════
# START WITH DELAY (device manages countdown)
# ════════════════════════════════════════════════════════
- variables:
_window_start: "{{ result.window.start | as_datetime }}"
# Washing machines use FinishInRelative
# (seconds from now until program must be finished)
_window_end: >
{{ (_window_start + timedelta(minutes=duration | int)).isoformat() }}
finish_in_relative: >
{% set window_end = _window_start + timedelta(minutes=duration | int) %}
{% set seconds_until_end = ((window_end - now()).total_seconds()) | int %}
{{ [duration | int * 60, seconds_until_end] | max }}
- action: home_connect_alt.start_program
target:
entity_id: "{{ program_entity }}"
data:
program: "{{ selected_program }}"
options:
- key: BSH.Common.Option.FinishInRelative
value: "{{ finish_in_relative }}"
- variables:
_n_title: "{{ title_planned }}"
_n_message: >
{{ selected_program.split('.')[-1] }}
{% set delay = finish_in_relative | int - (duration | int * 60) %}
{% if delay > 0 %}
· ⏰ ~{{ _window_start | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
(in {{ (delay / 3600) | round(1) }} h)
{% else %}
· ▶️ Starting now!
{% endif %}
· ~{{ duration }} min
· {{ result.window.price_mean | round(1) }} {{ result.price_unit }}/kWh
{% if _raw_duration in ['unknown', 'unavailable', 'None', ''] %}
· ⚠️ Duration estimated
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ notification_script | length > 0 }}"
sequence:
- action: script.turn_on
target:
entity_id: "{{ notification_script }}"
data:
variables:
event_type: planned
appliance: washing_machine
title: "{{ _n_title }}"
message: "{{ _n_message }}"
start_time: "{{ _window_start | as_local | as_timestamp | timestamp_custom('%Y-%m-%dT%H:%M:%S') }}"
duration_minutes: "{{ duration | int }}"
price_mean: "{{ result.window.price_mean | round(1) }}"
price_unit: "{{ result.price_unit }}"
selected_program: "{{ selected_program }}"
using_fallback_duration: "{{ _raw_duration in ['unknown', 'unavailable', 'None', ''] }}"
- conditions:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
sequence:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "{{ _n_title }}"
message: "{{ _n_message }}"

View file

@ -0,0 +1,233 @@
blueprint:
name: "Tibber Prices: Water Heater — Boost During Cheap Prices"
description: >
**Companion blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
(HACS integration)** · Blueprint v1.0.0
Automatically boost your water heater during the cheapest price
periods and return to eco temperature when prices rise.
**What it does:**
- Raises the water heater temperature during the Best Price Period
- Lowers it back to eco when the period ends
- Real-time reaction — no planning or helpers needed
**Prerequisites:**
- [Tibber Prices integration](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration) installed via HACS
- A `water_heater` entity (or `climate` entity for heat-pump boilers)
domain: automation
author: jpawlowski
homeassistant:
min_version: "2024.6.0"
source_url: https://github.com/jpawlowski/hass.tibber_prices/blob/main/custom_components/tibber_prices/blueprints/automation/tibber_prices/water_heater.yaml
input:
devices:
name: Devices
icon: mdi:water-boiler
description: Select your water heater and the Tibber Prices period sensor.
input:
period_sensor:
name: Best Price Period Sensor
description: >
The `binary_sensor.<home>_best_price_period` from Tibber Prices.
selector:
entity:
filter:
domain: binary_sensor
integration: tibber_prices
water_heater_entity:
name: Water Heater
description: >
Your water heater entity. Works with `water_heater.*`
or `climate.*` (for heat-pump water heaters).
selector:
entity:
filter:
domain:
- water_heater
- climate
temperatures:
name: Temperatures
icon: mdi:thermometer
description: Configure boost and eco temperatures.
input:
boost_temperature:
name: Boost Temperature
description: Target temperature during cheap prices.
default: 60
selector:
number:
min: 40
max: 80
step: 1
unit_of_measurement: °C
eco_temperature:
name: Eco Temperature
description: Target temperature outside cheap periods.
default: 45
selector:
number:
min: 30
max: 60
step: 1
unit_of_measurement: °C
runtime_overrides:
name: Runtime Overrides
icon: mdi:tune-vertical
collapsed: true
description: >
Optionally connect helpers to override settings from your
dashboard at runtime. When a helper is connected and has
a valid value, it takes priority over the fixed default.
Leave empty to always use the fixed defaults.
input:
boost_temperature_override:
name: "Override: Boost Temperature"
description: >
`input_number` helper to change the boost temperature
from your dashboard.
**Create in Settings → Helpers → Number**
(min: 40, max: 80, step: 1, unit: °C).
default: ""
selector:
entity:
filter:
domain: input_number
eco_temperature_override:
name: "Override: Eco Temperature"
description: >
`input_number` helper to change the eco temperature
from your dashboard.
**Create in Settings → Helpers → Number**
(min: 30, max: 60, step: 1, unit: °C).
default: ""
selector:
entity:
filter:
domain: input_number
notifications:
name: Notifications
icon: mdi:bell-outline
collapsed: true
description: >
Optional mobile notifications for temperature changes.
input:
notify_service:
name: Notification Service
description: >
One or more notify services, comma-separated
(e.g., `notify.mobile_app_yourphone` or
`notify.mobile_app_phone, notify.mobile_app_tablet`).
Leave empty to disable all notifications.
default: ""
selector:
text:
mode: restart
triggers:
- trigger: state
entity_id: !input period_sensor
to: "on"
id: period_start
- trigger: state
entity_id: !input period_sensor
to: "off"
id: period_end
variables:
_blueprint_variant: "water_heater"
water_heater_entity: !input water_heater_entity
_boost_temp_default: !input boost_temperature
_boost_temp_override: !input boost_temperature_override
boost_temperature: >
{% set o = _boost_temp_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | float(_boost_temp_default) }}
{% else %}
{{ _boost_temp_default }}
{% endif %}
_eco_temp_default: !input eco_temperature
_eco_temp_override: !input eco_temperature_override
eco_temperature: >
{% set o = _eco_temp_override %}
{% if o and states(o) not in ['unknown', 'unavailable'] %}
{{ states(o) | float(_eco_temp_default) }}
{% else %}
{{ _eco_temp_default }}
{% endif %}
notify_service: !input notify_service
actions:
# Check: Tibber Prices integration installed?
- variables:
_tp_entities: "{{ integration_entities('tibber_prices') | list }}"
- if:
- condition: template
value_template: "{{ _tp_entities | length == 0 }}"
then:
- stop: "Tibber Prices integration not found"
# ════════════════════════════════════════════════════════
# BOOST / ECO
# ════════════════════════════════════════════════════════
- choose:
- conditions:
- condition: trigger
id: period_start
sequence:
# Determine the correct service based on domain
- variables:
target_domain: "{{ water_heater_entity.split('.')[0] }}"
- action: "{{ target_domain }}.set_temperature"
target:
entity_id: "{{ water_heater_entity }}"
data:
temperature: "{{ boost_temperature }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔥 Water Heater — Boost Active"
message: >
Target raised to {{ boost_temperature }}°C during
the best price period.
- conditions:
- condition: trigger
id: period_end
sequence:
- variables:
target_domain: "{{ water_heater_entity.split('.')[0] }}"
- action: "{{ target_domain }}.set_temperature"
target:
entity_id: "{{ water_heater_entity }}"
data:
temperature: "{{ eco_temperature }}"
- if:
- condition: template
value_template: "{{ notify_service | length > 0 }}"
then:
- repeat:
for_each: "{{ notify_service.split(',') | map('trim') | reject('eq', '') | list }}"
sequence:
- action: "{{ repeat.item }}"
data:
title: "🔥 Water Heater — Back to Eco"
message: >
Best price period ended. Target back to
{{ eco_temperature }}°C.

View file

@ -0,0 +1,507 @@
blueprint:
name: "Tibber Prices: Notify Residents"
description: >
**Companion script blueprint for
[Tibber Prices](https://my.home-assistant.io/redirect/hacs_repository/?owner=jpawlowski&repository=hass.tibber_prices&category=integration)
appliance blueprints** · Blueprint v2.0.0
Advanced notification dispatcher that replaces the simple
"Quick Notification" in any Tibber Prices appliance blueprint.
**Features:**
- Up to **10 residents** — just pick a person, devices are
discovered automatically
- **Auto-discovery** — finds all Mobile App notify services
from the person's device trackers and notifies every device
- **Presence filtering** — only notify people who are home
- **iOS and Android** platform-specific options (interruption
level, notification channel, priority)
- **Notify service override** — for Telegram, groups, or any
non-mobile-app service
- Notifications **grouped by appliance** with smart tag
replacement (new events replace old ones)
**How to use:**
1. Create a new script from this blueprint
2. Add your residents — just select their person entity
3. In any Tibber Prices appliance blueprint, select this script
as **Notification Script (Advanced)**
4. Done! The appliance blueprint passes all context automatically
**Auto-discovery explained:** For each person, the script reads
the assigned device trackers (e.g., `device_tracker.alice_iphone`)
and derives the matching `notify.mobile_app_*` service
automatically. All devices of a person get notified — no manual
service configuration needed.
**Override:** If a person should receive notifications via
Telegram, a group, or a custom service instead of (or in addition
to) their mobile devices, set the optional "Notify Service
Override" field. When set, only the override service is used.
**Taking control:** Click "Take control" in the script editor
for full YAML access. The 10-slot limit no longer applies.
domain: script
author: jpawlowski
homeassistant:
min_version: 2024.6.0
input:
presence_settings:
name: Presence Settings
icon: mdi:home-account
description: >
Control whether notifications are filtered by who is home.
input:
filter_by_presence:
name: Only notify people who are home
description: >
When enabled, only residents whose person entity shows
`home` will receive the notification.
Disabled = everyone gets notified regardless of location.
default: true
selector:
boolean:
resident_1:
name: "Resident 1"
icon: mdi:account
description: >
First notification recipient. Select the person entity —
their mobile devices are discovered automatically.
input:
resident_1_person:
name: "Resident 1 — Person"
description: >
Person entity (e.g., `person.alice`).
Leave empty to skip this slot.
default: ""
selector:
entity:
filter:
domain: person
resident_1_override:
name: "Resident 1 — Notify Service Override"
description: >
Optional: a specific notify service to use instead of
auto-discovered mobile devices (e.g.,
`notify.telegram_alice` or `notify.family_group`).
When set, auto-discovery is skipped for this resident.
default: ""
selector:
text:
resident_2:
name: "Resident 2"
icon: mdi:account
collapsed: true
description: "Second notification recipient."
input:
resident_2_person:
name: "Resident 2 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_2_override:
name: "Resident 2 — Notify Service Override"
default: ""
selector:
text:
resident_3:
name: "Resident 3"
icon: mdi:account
collapsed: true
description: "Third notification recipient."
input:
resident_3_person:
name: "Resident 3 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_3_override:
name: "Resident 3 — Notify Service Override"
default: ""
selector:
text:
resident_4:
name: "Resident 4"
icon: mdi:account
collapsed: true
description: "Fourth notification recipient."
input:
resident_4_person:
name: "Resident 4 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_4_override:
name: "Resident 4 — Notify Service Override"
default: ""
selector:
text:
resident_5:
name: "Resident 5"
icon: mdi:account
collapsed: true
description: "Fifth notification recipient."
input:
resident_5_person:
name: "Resident 5 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_5_override:
name: "Resident 5 — Notify Service Override"
default: ""
selector:
text:
resident_6:
name: "Resident 6"
icon: mdi:account
collapsed: true
description: "Sixth notification recipient."
input:
resident_6_person:
name: "Resident 6 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_6_override:
name: "Resident 6 — Notify Service Override"
default: ""
selector:
text:
resident_7:
name: "Resident 7"
icon: mdi:account
collapsed: true
description: "Seventh notification recipient."
input:
resident_7_person:
name: "Resident 7 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_7_override:
name: "Resident 7 — Notify Service Override"
default: ""
selector:
text:
resident_8:
name: "Resident 8"
icon: mdi:account
collapsed: true
description: "Eighth notification recipient."
input:
resident_8_person:
name: "Resident 8 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_8_override:
name: "Resident 8 — Notify Service Override"
default: ""
selector:
text:
resident_9:
name: "Resident 9"
icon: mdi:account
collapsed: true
description: "Ninth notification recipient."
input:
resident_9_person:
name: "Resident 9 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_9_override:
name: "Resident 9 — Notify Service Override"
default: ""
selector:
text:
resident_10:
name: "Resident 10"
icon: mdi:account
collapsed: true
description: "Tenth notification recipient."
input:
resident_10_person:
name: "Resident 10 — Person"
default: ""
selector:
entity:
filter:
domain: person
resident_10_override:
name: "Resident 10 — Notify Service Override"
default: ""
selector:
text:
# ════════════════════════════════════════════════════════════
# Script fields — received from the appliance blueprint
# via script.turn_on → data → variables
# ════════════════════════════════════════════════════════════
fields:
event_type:
name: Event Type
description: >
What happened. Values: setup_required, not_ready, no_program,
no_window, planned, started, prepare_washer, timeout,
invalid_loads, wash_planned, wash_done, dryer_planned,
dryer_skipped, cancelled, complete.
required: true
example: planned
selector:
text:
appliance:
name: Appliance
description: >
Which appliance sent this. Values: dishwasher,
washing_machine, dryer, laundry_pipeline.
required: true
example: dishwasher
selector:
text:
title:
name: Title
description: Default notification title (with emoji).
required: true
example: "🍽️ Dishwasher — Planned!"
selector:
text:
message:
name: Message
description: Default notification message body.
required: true
example: "Starts at 02:15 (in 3.2 h). Duration: ~120 min."
selector:
text:
start_time:
name: Start Time
description: >
ISO start time (planned/wash_planned/dryer_planned events).
example: "2025-01-15T02:15:00"
selector:
text:
duration_minutes:
name: Duration (minutes)
description: >
Program duration in minutes (planned/no_window events).
example: "120"
selector:
text:
price_mean:
name: Average Price
description: >
Mean price in the selected window (planned events).
example: "18.5"
selector:
text:
price_unit:
name: Price Unit
description: >
Currency unit (planned events), e.g., "ct/kWh" or "øre/kWh".
example: "ct/kWh"
selector:
text:
selected_program:
name: Selected Program
description: >
Appliance program name (planned events, Home Connect Alt only).
example: "Dishcare.Dishwasher.Program.Eco50"
selector:
text:
using_fallback_duration:
name: Using Fallback Duration
description: >
"True" if the duration is a fallback estimate (planned events).
example: "False"
selector:
text:
deadline:
name: Deadline
description: >
The deadline that was exceeded (no_window events).
example: "2025-01-15T08:00:00"
selector:
text:
load_index:
name: Load Index
description: >
Current load number (pipeline events).
example: "2"
selector:
text:
total_loads:
name: Total Loads
description: >
Total number of loads planned (pipeline events).
example: "3"
selector:
text:
# ════════════════════════════════════════════════════════════
# Variables — map blueprint inputs to template variables
# ════════════════════════════════════════════════════════════
variables:
filter_by_presence: !input filter_by_presence
r1_person: !input resident_1_person
r1_override: !input resident_1_override
r2_person: !input resident_2_person
r2_override: !input resident_2_override
r3_person: !input resident_3_person
r3_override: !input resident_3_override
r4_person: !input resident_4_person
r4_override: !input resident_4_override
r5_person: !input resident_5_person
r5_override: !input resident_5_override
r6_person: !input resident_6_person
r6_override: !input resident_6_override
r7_person: !input resident_7_person
r7_override: !input resident_7_override
r8_person: !input resident_8_person
r8_override: !input resident_8_override
r9_person: !input resident_9_person
r9_override: !input resident_9_override
r10_person: !input resident_10_person
r10_override: !input resident_10_override
# Build a flat list of {service, person} notification targets.
# For each resident with a person entity set:
# - If an override service is configured → use that
# - Otherwise → auto-discover mobile_app notify services
# from the person's device_trackers attribute
notify_targets: >
{% set slots = [
{'person': r1_person, 'override': r1_override},
{'person': r2_person, 'override': r2_override},
{'person': r3_person, 'override': r3_override},
{'person': r4_person, 'override': r4_override},
{'person': r5_person, 'override': r5_override},
{'person': r6_person, 'override': r6_override},
{'person': r7_person, 'override': r7_override},
{'person': r8_person, 'override': r8_override},
{'person': r9_person, 'override': r9_override},
{'person': r10_person, 'override': r10_override},
] %}
{% set ns = namespace(targets=[]) %}
{% for slot in slots if slot.person != '' %}
{% set override = slot.override | default('') %}
{% if override | length > 0 %}
{% set ns.targets = ns.targets
+ [{'service': override, 'person': slot.person}] %}
{% else %}
{% set trackers = state_attr(slot.person,
'device_trackers') or [] %}
{% for t in trackers %}
{% set dev_name = t.split('.')[1] %}
{% if services.notify is defined
and 'mobile_app_' ~ dev_name in services.notify %}
{% set ns.targets = ns.targets
+ [{'service': 'notify.mobile_app_' ~ dev_name,
'person': slot.person}] %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{{ ns.targets }}
# Events that bypass presence filtering (always notify everyone)
critical_events:
- complete
- cancelled
- timeout
icon: mdi:bell-ring
mode: parallel
max: 10
sequence:
- repeat:
for_each: "{{ notify_targets }}"
sequence:
# ── Presence check ──────────────────────────────
- condition: template
value_template: >
{% set person_id = repeat.item.person %}
{% if not filter_by_presence %}
true
{% elif event_type in critical_events %}
true
{% else %}
{{ states(person_id) == 'home' }}
{% endif %}
# ── Send notification ───────────────────────────
- action: "{{ repeat.item.service }}"
data:
title: "{{ title }}"
message: "{{ message }}"
data:
# iOS — interruption level
push:
interruption-level: >
{% if event_type in ['planned', 'wash_planned',
'dryer_planned', 'complete'] %}
time-sensitive
{% else %}
active
{% endif %}
# Android — channel and priority
channel: tibber_prices
importance: >
{% if event_type in ['planned', 'wash_planned',
'dryer_planned', 'complete'] %}
high
{% else %}
default
{% endif %}
ttl: 0
priority: high
# Group & replace — new events replace old ones
group: "tibber_{{ appliance }}"
tag: "tibber_{{ appliance }}_{{ event_type }}"