mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
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:
parent
2d2873f75f
commit
e75e0ed1dc
19 changed files with 8627 additions and 0 deletions
|
|
@ -7,6 +7,8 @@ https://github.com/jpawlowski/hass.tibber_prices
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
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:
|
||||
"""Set up the Tibber Prices component from configuration.yaml."""
|
||||
# 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")
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -366,6 +418,11 @@ async def async_remove_entry(
|
|||
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}")
|
||||
|
||||
# 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(
|
||||
hass: HomeAssistant,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 1–2 hours, but prices remain favorable for 4–6 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.
|
||||
|
|
@ -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 (0–100%). 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.
|
||||
|
|
@ -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 1–5 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 (1–5) 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 }}"
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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.
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 }}"
|
||||
Loading…
Reference in a new issue