Skip to main content
  1. Tutorials/

Dynamic HVAC Scheduling with Node-RED and Home Assistant

Status In Progress
Difficulty Deep dive
Time ~90 min
Stack
Home Assistant Node-RED SRP

This tutorial builds a professional-grade HVAC scheduler in Node-RED that handles seasonal changes, peak utility hours, occupancy, and guest overrides. It replaces rigid thermostat schedules with visual logic that actually follows your life.

Executive Summary
#

Home Assistant is great for simple tasks, but HVAC logic often requires several checks at once. Node-RED lets you visualize all of that logic on a canvas so you can see exactly why a temperature was set. When something goes wrong, you can trace the message through the flow and spot the problem quickly instead of guessing.

The finished flow handles four things automatically:

  • Time-of-Use windows that adjust setpoints during peak rate hours
  • Seasonal switching between Winter and Summer logic based on the calendar month, with no manual input required
  • Occupancy checks that stop the AC if the house is empty or on vacation
  • Guest overrides that let guests change the temperature without the automation fighting them

Prerequisites
#

ComponentRequirement
PlatformHome Assistant (OS or Supervised)
Add-onNode-RED (available in the HA Add-on Store)
DeviceAny climate entity (Nest, Ecobee, Z-Wave, etc.)
HelpersTwo toggles: Guest Mode and Thermostat Schedule Enabled

Implementation
#

Step 1: Download the Flow
#

You can import the finished flow directly into Node-RED instead of building it from scratch.

Download srp_tou_full_schedule.json

To import it, click the hamburger menu in the top right corner of Node-RED and select Import:

The Node-RED hamburger menu open with Import highlighted.
The hamburger menu is the three horizontal lines next to the Deploy button. Import is near the top of the list.

Paste the JSON or click “select a file to import” and choose the downloaded file. Make sure new flow is selected at the bottom so it lands on its own tab:

The Node-RED Import dialog with JSON pasted into the clipboard field and the new flow option visible at the bottom.
Click Import in the bottom right when ready. The flow will appear as a new tab called SRP TOU Full Schedule.

The flow will land as a new tab called SRP TOU Full Schedule. You will still need to update the entity IDs to match your own setup. The steps below walk you through exactly what each part does and what to change.

Here is the big picture. The flow has four distinct sections:

The complete Node-RED flow showing Winter and Summer schedule groups on the left, the guest override group top right, and the Safety Stack along the bottom right.
Blue group on the top left is the Winter schedule. Pink group on the bottom left is Summer. Everything funnels into the Safety Stack on the right before touching the thermostat.

Step 2: Create Your Helpers
#

Helpers are small virtual switches that Node-RED reads to understand the current state of your home. In Home Assistant, go to Settings > Devices & Services > Helpers.

You need two Toggle helpers:

  • input_boolean.guest_mode — when this is on, the guest override logic fires and pauses the main schedule
  • input_boolean.thermostat_schedule_automation_enabled — the master on/off switch for the entire flow

You do not need a Season dropdown. The flow determines the season automatically by reading the current month from the system clock.

The Home Assistant Helpers page filtered to “guest mod”, showing Guest Mode and Guest Mode Override as Input boolean helpers.
Guest Mode and Guest Mode Override both need to exist. The Entity ID column shows the exact string Node-RED uses to reference them.

The Home Assistant Helpers page filtered to “vacat”, showing Vacation Mode as an Input boolean helper.
Vacation Mode is checked by the first door in the Safety Stack. If it does not exist, that node will error.

The Home Assistant Helpers page filtered to “sched”, showing Thermostat Schedule Automation Enabled as an Input boolean helper.
This is the master switch. The Guest Override logic turns this off and on automatically, but you can also toggle it manually from the dashboard.

Step 3: Set Up the Time Triggers
#

Each schedule transition is driven by an Inject node set to fire at a specific time of day. The number in the node name is the temperature setpoint passed along as the message payload.

  1. Drag an Inject Node onto the canvas
  2. Set the payload type to Number
  3. Enter the target temperature as the value (for example, 76)
  4. Set the repeat mode to at a specific time and enter the trigger time
  5. Name the node to match, such as 1:30 PM - 76

The finished Summer Weekday schedule looks like this:

TimeSetpointReason
12:00 AM81°FOvernight economy
8:00 AM79°FMorning warmup
8:30 AM78°FRamp down
9:00 AM77°FComfortable day
1:30 PM76°FPre-cool before peak
2:00 PM80°FLoad shed during peak
8:00 PM78°FPost-peak recovery
9:00 PM77°FEvening comfort
11:30 PM80°FLate night economy

Step 4: Build the Season and Day Filters
#

Each group of time triggers connects to a Function node that checks two things at once: what month it is, and whether today is a weekday or a weekend. There are four filter functions in total.

Summer Weekday Filter:

const d = new Date();
const m = d.getMonth();
const day = d.getDay();
if ((m >= 4 && m <= 9) && (day >= 1 && day <= 5)) return msg;
return null;

Summer Weekend Filter:

const d = new Date();
const m = d.getMonth();
const day = d.getDay();
if ((m >= 4 && m <= 9) && (day === 0 || day === 6)) return msg;
return null;

Winter Weekday Filter:

const d = new Date();
const m = d.getMonth();
const day = d.getDay();
if ((m <= 3 || m >= 10) && (day >= 1 && day <= 5)) return msg;
return null;

Winter Weekend Filter:

const d = new Date();
const m = d.getMonth();
const day = d.getDay();
if ((m <= 3 || m >= 10) && (day === 0 || day === 6)) return msg;
return null;

The Node-RED function node editor open, showing the Winter WD Filter code in the On Message tab.
Double-click any function node to open this editor. Paste your filter code into the On Message tab.

getMonth() returns 0 for January through 11 for December. getDay() returns 0 for Sunday through 6 for Saturday. Month 4 is May and month 9 is September, covering the Arizona summer peak season. If the current date does not match a filter’s conditions, the function returns null and the message stops there.

Step 5: Build the Safety Stack
#

Every message from every filter funnels into the same three-node Safety Stack before anything reaches the thermostat. Think of these as doors that must all be unlocked in order.

A close-up of the four Safety Stack nodes: Vacation OFF?, Anyone Home?, Schedule Enabled?, and Apply Temperature, connected left to right with status text visible under each.
The status text under each node shows its last known state. Notice Schedule Enabled? has a red indicator, meaning the master schedule was off at that moment and the flow stopped there.

  • Door 1: Vacation OFF? Reads input_boolean.vacation_mode. If vacation mode is on, the message is dropped.
  • Door 2: Anyone Home? Reads binary_sensor.home_occupied. If the house is empty, the message is dropped.
  • Door 3: Schedule Enabled? Reads input_boolean.thermostat_schedule_automation_enabled. If a guest has taken manual control and flipped this off, the message stops here.

Only a message that passes all three checks reaches the final node.

Step 6: Wire Up the Apply Temperature Node
#

Once all three doors are open, the message reaches the Apply Temperature action node. This is the only node that actually talks to your thermostat.

  1. Drag out a Call Service node
  2. Set the Domain to climate and the Service to set_temperature
  3. Select your thermostat entity
  4. In the Data field, enter:
{ "temperature": {{payload}} }

The Apply Temperature action node editor showing the climate.set_temperature action, the Thermostat entity target, and the JSON data field.
The Data field is where the temperature lands. The {{payload}} value is whatever number the Inject node sent.

The Data field dropdown open showing the expression and JSON options, with JSON highlighted.
Make sure the Data field type is set to JSON, not expression. Expression mode will not handle {{payload}} the same way.

Step 7: Configure Guest Override Logic
#

The Guest Override group runs independently of the main schedule. It watches for state changes on input_boolean.guest_mode and input_boolean.guest_mode_override and manages the master schedule toggle automatically.

EventAction
Guest Mode turns OFFReset Guest Override, re-enable Master Schedule
Guest Override turns OFFRe-enable Master Schedule
Guest Override turns ONDisable Master Schedule

When a guest manually adjusts the thermostat and triggers the override, the master schedule is paused so the automation does not undo their change at the next trigger. When Guest Mode is turned off, everything resets automatically.

Lab Notes & Troubleshooting
#

The thermostat is not changing. Open the Debug panel in Node-RED and attach a Debug node after the Season Filter function. If messages are arriving at the filter but not passing through, check that your server’s system time and timezone are correct.

The schedule runs during vacation. The entity ID in the Current State node must match your helper exactly. Capitalization matters. Copy and paste the entity ID directly from Home Assistant to avoid typos.

The inject nodes did not fire. Standard Inject nodes trigger at an exact second. If your server was rebooting at that moment, the trigger may have been missed. Click the small button on the left side of any Inject node to fire it manually and test the flow without waiting.

Summary
#

You now have a fully automated HVAC scheduler that handles season detection, peak rate windows, occupancy, and guest overrides without any manual intervention. The Safety Stack ensures the thermostat is never touched unless all conditions are met. Once the flow is stable, consider adding humidity offsets, window sensors, or dynamic pricing data to take it further.


Disclaimer: Content on this site is provided as-is with no guarantees of accuracy, completeness, or fitness for any particular purpose. Always test in a safe, non-production environment before applying anything documented here to systems you rely on. I am not responsible for data loss, damage, or security issues resulting from following these guides. Software and services change over time and steps that worked when this was written may not work in the future.