A Home Assistant custom integration that finds the cheapest (or most expensive) upcoming hours based on
Nordpool electricity prices to save money automatically.
- Price-based scheduling — Automatically activates the cheapest/most expensive hours of the day
- Always-cheap threshold — Slots below/above a price threshold are always activated
- Always-expensive threshold — Safety cutoff to never activate above/below a certain price
- Price similarity threshold — Groups slots with nearly identical prices for more natural scheduling
- Rolling window constraint — Ensures minimum activity within any configurable time window (e.g., water heater must run at least 4 hours in any 24-hour window)
- Minimum consecutive hours — Prevents short on/off cycles by requiring a minimum run duration
- Excluded hours — Block a time range from ever being activated (e.g., to avoid grid fee peak hours)
- Multiple instances — Add one per appliance (water heater, floor heating, pool pump, etc.)
- Always on / Always off — Force all controlled entities ON or OFF via switches, bypassing the schedule
- Emergency mode — Keeps appliances running if price data is unavailable
- No helpers needed — All configuration is done through the integration's UI
- Home Assistant 2024.4.0 or newer
- Nord Pool integration, either the native addon or the HACS custom addon, installed and configured
- Click the button above, or search for Power Saver in HACS
- Click Install
- Restart Home Assistant
- Copy the
custom_components/power_saverfolder to your Home Assistantcustom_componentsdirectory - Restart Home Assistant
Click the button above, or add it manually:
- Go to Settings → Devices & Services → Add Integration
- Search for Power Saver
- Fill in the configuration:
| Field | Description |
|---|---|
| Nord Pool sensor | The Nord Pool sensor to use for electricity prices |
| Name | A descriptive name (e.g., "Water Heater", "Floor Heating") |
| Mode | Cheapest selects the cheapest hours; Most expensive selects the most expensive (inverts the schedule) |
| Rolling window hours | Ensures minimum hours within a rolling window (default = 24) |
| Minimum active hours | How many hours per day the appliance should run |
| Always-cheap price | Price below which slots are always active (empty = disabled) |
| Always-expensive price | Price at/above which slots are never active (empty = disabled) |
| Price similarity threshold | Prices within this range are treated as equal (empty = disabled) |
| Minimum consecutive active hours | Minimum number of hours to keep active in a row (empty = disabled) |
| Exclude from / Exclude until | Time range during which slots are never activated and ignored by the scheduler. Useful for avoiding hours with extra grid fees. Supports cross-midnight ranges (e.g., 22:00 to 06:00). Both fields must be set to enable (empty = disabled) |
| Controlled entities | One or more switch, input_boolean, or light entities to turn on/off automatically (empty = disabled) |
- Click Submit
To add another appliance, simply add a new service with different settings.
All scheduling parameters can be changed at any time via Settings → Devices & Services → Power Saver → Configure. Changes take effect immediately.
Each instance creates the following sensors:
| State | Description |
|---|---|
active |
The appliance should be running in the current time slot |
standby |
The appliance should be off in the current time slot |
excluded |
The slot is in the excluded time range and will never activate |
forced_on |
Always on is active — all controlled entities are forced ON |
forced_off |
Always off is active — all controlled entities are forced OFF |
| Attribute | Description |
|---|---|
current_price |
Electricity price for the current time slot |
min_price |
Lowest price today |
max_price |
Highest price today |
active_slots |
Total number of active slots in the schedule |
Each instance includes two override switches:
- Always on — Forces all controlled entities ON regardless of the schedule. The status sensor shows
forced_on. - Always off — Forces all controlled entities OFF regardless of the schedule. The status sensor shows
forced_off.
The two switches are mutually exclusive — enabling one automatically disables the other. Turn both OFF to resume normal scheduling. Switch states persist across Home Assistant restarts.
| Sensor | Type | Description |
|---|---|---|
| Schedule | Sensor | Full schedule with all time slots, prices, and statuses |
| Last Active | Sensor | Timestamp of the last active slot |
| Active Hours in Window | Sensor | Scheduled active hours in the upcoming rolling window |
| Next Change | Sensor | Timestamp of the next state transition (displayed as relative time) |
| Emergency Mode | Binary sensor | Indicates if running without price data (problem badge) |
- Price data — Reads prices from your Nord Pool sensor (today + tomorrow when available)
- Excluded hours (optional) — Marks slots in the excluded time range as permanently off, removing them from scheduling
- Slot selection — Selects the cheapest (or most expensive) slots from the remaining hours to meet your minimum active hours
- Thresholds — Applies always-cheap (force on) and always-expensive (force off) price thresholds
- Similarity grouping — Groups slots with nearly identical prices for more consistent scheduling
- Rolling window (optional) — Ensures minimum activity within any rolling time window, activating additional slots as needed
- Consecutive hours (optional) — Merges short active segments to prevent rapid on/off cycling
- Updates — Recalculates every 15 minutes and immediately when new prices arrive
You can visualize the electricity price alongside the Power Saver schedule using the ApexCharts Card for Home Assistant. This gives you a clear overview of when the appliance is active and how it correlates with the price.
Replace sensor.heater_power_saver_schedule with your own schedule sensor entity ID.
ApexCharts card YAML
type: custom:apexcharts-card
header:
show: true
title: Price 48t + Powersaver
now:
show: true
label: Now
graph_span: 2d
span:
start: day
apex_config:
stroke:
width: 2
dataLabels:
enabled: true
fill:
type: gradient
gradient:
shadeIntensity: 1
inverseColors: false
opacityFrom: 0.45
opacityTo: 0.05
stops:
- 10
- 50
- 75
- 1000
legend:
show: false
yaxis:
- id: price
show: true
decimalsInFloat: 1
floating: false
forceNiceScale: true
extend_to: end
- id: usage
show: true
opposite: true
decimalsInFloat: 0
floating: false
forceNiceScale: true
extend_to: end
- id: powersaver
show: false
decimalsInFloat: 0
floating: false
extend_to: now
series:
- entity: sensor.heater_power_saver_schedule
yaxis_id: price
extend_to: now
name: Price
type: area
curve: stepline
color: tomato
show:
legend_value: false
data_generator: |
return entity.attributes.schedule.map((entry) => {
return [new Date(entry.time).getTime(), entry.price];
});
- entity: sensor.heater_power_saver_schedule
data_generator: |
return entity.attributes.schedule.map((entry) => {
return [new Date(entry.time).getTime(), entry.status === "active" ? 1 : 0];
});
yaxis_id: powersaver
name: " "
type: area
color: rgb(0, 255, 0)
opacity: 0.2
stroke_width: 0
curve: stepline
group_by:
func: min
show:
legend_value: false
in_header: false
name_in_header: false
datalabels: falseThe vast majority of this project was developed by an AI assistant. While I do have some basic experience with programming from a long time ago, I'm essentially the architect, guiding the AI, fixing its occasional goofs, and trying to keep it from becoming self-aware.
