Introduction: Time-of-Use Air Conditioning With a Vera Smart Home Controller

This Instructable will show how to run your home HVAC less often during "Time-of-Use" peak hours and potentially cut power bills in half on extreme days. I implemented this using a VeraLite smart home controller and tested with both Z-Wave and Nest thermostats.

How does it work? Just imagine during peak hours repeatedly switching your thermostat to "Cool" for 5 minutes then to "Off" for 10 minutes. That's all your HVAC needs anyway on a moderate day but on very hot days it's not enough. That means you will get regular bursts of cool air but your inside temperature will start to slowly rise. A little less comfortable with a fraction of the cost.

This is automated by coding a Vera scene to run every minute and switch between modes. Scenes are written in Vera's Luup (Lua-UPnP) language and can be triggered to run based on timers or events. It works for both heating and cooling and allows customization based on your power company's time-of-use plan, which may have different costs based on time of day, day of week, or month.


You will need a working knowledge of coding, create and running Vera scenes, and debugging when things go wrong. I provide basic working code examples but you must modify them for your application. To get started you'll need:

  1. Details of your power company's "Time-of-Use" plan
  2. Vera smart home controller, like the VeraLite ($99)
  3. Any Vera compatible thermostat - I have specifically tested this on:

It would be straightforward to modify these concepts to SmartThings and other controllers. Feel free to copy, share, and create your own Instructable to improve upon these ideas.


We received a Nest Thermostat as a wedding present (awesome friends, right?) and it's a cool gadget but it really hasn't saved much money. Someone is home almost all the time so Auto-Away rarely kicks in, and hot California summers either keep the AC running all the time - or not at all if you set it too high. With Pacific Gas & Electric with rates ranging from $0.10 to $0.50 per kWh that can be $30 a day. Ouch!

As you can see from the picture above I have been able to flatten those costs out during peak hours. In spite of the high temperature of 107F on Wednesday July 29th 2015 the daily cost was about $12.50.

Step 1: Time-of-Use Power Plan

WARNING:Proceed with caution, you can easily mess this up and end up paying more. Learn from my mistakes!

Call your power company or get on their website and find out what's offered. I'm in rural California so with PG&E I found:

In my case I read through this info and found the PG&E "E-6" plan was available to me and fit within what I was trying today. It has different schedules for summer vs. winter, weekdays vs. weekends and holidays, and three different rates: Off Peak, Partial Peak, and Full Peak.


You might have to wait a full billing cycle to change to a Time-of-Use plan - and a full billing cycle to change back! If you find out that you're using more power than expected during peak hours you could end up with a hefty bill.


Even though you've selected a Time-of-Use plan you may still have Tiered pricing that applies. For example with the PG&E "E-6" plan the peak price looks to be about $0.30/kWh - but during a billing cycle after you consume some amount of kWh at that rate the price ratchets up 50%. You will have to factor that into your cost savings calculations later on.


Don't be too aggressive with your power plan until you understand exactly how it works. I originally picked the PG&E "E-6+SmartRate" plan that promised 20% better savings than "E-6" - with the restriction that between 9 and 15 days a year the peak power would have an additional surcharge

The very first day of my plan was 105 degrees and I got an email that I would be charged $0.60 additional for each Full Peak kWh. I would have to turn the AC completely off to save any money. Luckily I only had to wait a day to turn the "SmartRate" option off.

Step 2: Vera Scene - Initialize Device IDs

To start creating the Vera scene code you will need to know the device ID for the thermostat you want to control. The screenshot shows my Nest thermostat using Vera firmware UI7, which is slightly different than earlier versions. In all versions simply click on a device's details (>) and look in the "Advanced" tab to find the device ID. In my case the thermostat is device #39.

You can also add any number of thermostats or other Z-Wave switches to toggle during peak hours. In my setup power to the attic fan is controlled by device #63.

Open up a text editor and start writing your scene code:

local id_hvac1 = 39
local id_switch1 = 63

OPTION: Using a Nest Thermostat

If you want to use a Nest Thermostat install and configure the Vera Nest Plugin. However, unlike Z-Wave thermostats which are locally sent over Z-Wave, Nest commands require an internet connection - and may occasionally fail to work reliably. The Nest could fail to receive commands and may will cost you extra during peak hours.

Step 3: Vera Scene - Time-of-Use Targets

You will now need to decide how much you want your HVAC to run during each time-of-use period. The scene will run one at one minute intervals to check the time-of-use policy based on these settings:

local tou_ticks = 15        -- Ticks per cycle, must be <60 minutes

local tou_fullpeak = {t="FullPeak", l=5}
local tou_partpeak = {t="PartPeak", l=8}
local tou_cooldown = {t="CoolDown", l=10}
local tou_holiday  = {t="Holiday",  l=tou_ticks}
local tou_offpeak  = {t="OffPeak",  l=tou_ticks}

For example, I decided to use a 15 minute cycle and during "FullPeak" the HVAC can only run 5 minutes - or 33% of the time. I don't recommend running less than 5 minutes at a time as this can put undue stress on your HVAC system.


The "FullPeak", "PartialPeak", "OffPeak", and "Holiday" correspond to the power company's time-of-use policy. But what about "CoolDown"?

Through experimentation I found on very hot days as we went peak to off-peak the air conditioner would run for almost 3 hours straight as it tried to cool down a warmer than normal house. This became almost uncomfortably cold.

When I noticed the kids wrapped up in blankets on summer evenings I realized I could improve this.

During "CoolDown" I continue to artificially limit the HVAC even after PG&E rates have dropped to the lowest. The outside air temperature will slowly cool down, and I just elect to have the HVAC not work as hard to try and get back on track. This only has any meaningful difference on extreme temperature days.

Step 4: Vera Scene - Time of Use Policy Definition

The next bit of code defines the time-of-use policy based on time and date. Summer and winter have different rules for full and partial peak, and weekends are off peak. Note that to simplify the tables some entries overlap - for example summer (months 5-10) are checked first, then winter next (months 1-12). Same for weekdays vs. weekends, and bookending partial peak around full peak on summer weekdays.

local tou_periods = {
    {toy="Summer", fmonth=5, lmonth=10,
        {tow="Weekday", fwday=2, lwday=6,
            {b=13, e=19, p=tou_fullpeak},
            {b=10, e=21, p=tou_partpeak},
            {b=21, e=24, p=tou_cooldown}},
        {tow="Weekend", fwday=1, lwday=7,
            {b=17, e=20, p=tou_partpeak},
            {b=20, e=23, p=tou_cooldown}}},
    {toy="Winter", fmonth=1, lmonth=12,
        {tow="Weekday", fwday=2, lwday=6,
            {b=17, e=20, p=tou_partpeak},
            {b=20, e=23, p=tou_cooldown}}}}

Holidays vary from year to year so this code will need maintenance once a year. If you like you can ignore this section and do more aggressive limiting on holiday days:

--  2015 Holidays - 1/1, 2/16, 5/25, 7/4, 9/7, 11/11, 11/26, 12/25
local tou_holidays = {
    {m=1,d=1}, {m=2,d=16}, {m=5,d=25}, {m=7,d=4},
    {m=9,d=7}, {m=11,d=11}, {m=11,d=26}, {m=12,d=25}}

Step 5: Vera Scene - Find Policy Function

Now here's a function that takes the current date/time plus Lua tables for the holidays and periods and finds the match, or "OffPeak" if none is found. I coded this as a function because future extensions (coming soon!) will be calling this from multiple places:

function tou_find_policy(dt, holidays, periods)
    -- Holidays by month/date
    for ihol,hol in ipairs(holidays) do
        if (dt.month == hol.m) and ( == hol.d) then
            return tou_holiday
    -- Periods by time-of-year/time-of-week/time-of-day
    for itoy,toy in ipairs(periods) do
        if (dt.month >= toy.fmonth) and (dt.month <= toy.lmonth) then
            for itow,tow in ipairs(toy) do
                if (dt.wday >= tow.fwday) and (dt.wday <= tow.lwday) then
                    for itod,tod in ipairs(tow) do
                        if (dt.hour >= tod.b) and (dt.hour < tod.e) then
                            return tod.p

    return tou_offpeak

Step 6: Vera Scene - HVAC Tick Function

For each time this scene is called we call the tou_hvac_tick() function to toggle thermostat modes and switches based on current tick versus ticks allowed per the time-of-use policy. If you have multiple thermostats you can call this function once for each. To start we define parameters and look at the current mode (Off/Cool/Heat/Auto) and state (Idle, Cooling, Heating) of the thermostat:

function tou_hvac_tick(tick, policy, dt, id)
    local urn_hvac_mode = "urn:upnp-org:serviceId:HVAC_UserOperatingMode1"
    local urn_hvac_state = "urn:micasaverde-com:serviceId:HVAC_OperatingState1"
    local mode_status = luup.variable_get(urn_hvac_mode, "ModeStatus", id)
    local mode_state = luup.variable_get(urn_hvac_state, "ModeState", id)

To make your thermostat work as normally as possible it earns a credit when on but idle and loses a credit otherwise. If the thermostat has credits then it isn't limited, meaning on a moderate day you won't see the thermostat constantly cycling between off and active. If it does start cooling or heating it will do so until it runs out of credits, meaning if you arrive home and the house is too hot you can crank down the temperature and give it a boost without immediately starting limiting.

    -- TimeOfUseCredits: Increment if thermostat on but idle; decrement if active
    local var_credits = tonumber(luup.variable_get("TimeOfUse", "TimeOfUseCredits", id) or 0)

    if (mode_status ~= "Off") then
        if (mode_state == "Idle") and (var_credits < policy.l) then
            var_credits = var_credits + 1
        elseif (mode_state ~= "Idle") and (var_credits > 0) then
            var_credits = var_credits - 1

The difficult part here is dealing with occasional failure to transmit Z-Wave or Nest thermostat mode change requests. When switching a thermostat off we record the current mode as the "TimeOfUsePrevious" variable of the thermostat device. Then when resuming we set "ModeTarget" to this value and on the next tick verify "ModeStatus" to see that the command took. Only then do we clear the previous mode to "None".

    -- TimeOfUsePrevious: Set "Off" if active or previous mode when limited 
    local var_previous = luup.variable_get("TimeOfUse", "TimeOfUsePrevious", id) or "None"

    if (tick >= policy.l) and (var_credits == 0) then
        if (var_previous == "None") then
            luup.variable_set("TimeOfUse", "TimeOfUsePrevious", mode_status, id)
            luup.call_action(urn_hvac_mode, "SetModeTarget",
                    {NewModeTarget = "Off"}, id)

    elseif (var_previous ~= "None") then
        if (var_previous == mode_status) then
            luup.variable_set("TimeOfUse", "TimeOfUsePrevious", "None", id)
        luup.call_action(urn_hvac_mode, "SetModeTarget",
                {NewModeTarget = var_previous}, id)

Lastly save the new credit count. For debugging purposes I also log the time of use policy as a variable on the thermostat device and end the function:

    luup.variable_set("TimeOfUse", "TimeOfUseCredits", var_credits, id)
    luup.variable_set("TimeOfUse", "TimeOfUsePolicy", policy.t, id)

Step 7: Vera Scene - Switch Tick Function

If you want to limit normal Z-wave switches code up this function and call once for each switch device ID:

function tou_switch_tick(tick, policy, id)
    local urn_switch = "urn:upnp-org:serviceId:SwitchPower1"
    local switch_target = 1
    if (tick > policy.l) then
        switch_target = 0
    luup.call_action(urn_switch , "SetTarget",
            {newTargetValue = switch_target}, id)

Step 8: Vera Scene - Processing Devices

And last but not least, call the functions we defined to get the current policy and process ticks for HVAC and switch devices in your system:

local curr_time ='*t')
local curr_tick = ((curr_time.hour*60) + curr_time.min) % tou_ticks
local curr_policy = tou_find_policy(curr_time, tou_holidays, tou_periods)

tou_hvac_tick(curr_tick, curr_policy, curr_time, id_hvac1)

tou_switch_tick(curr_tick, curr_policy, id_switch1)

return true

Step 9: Triggering the Scene

The last step is to create a Vera scene and schedule it to trigger every 1 minute and have it execute the Luup scene code we created. I've attached the complete sample code here for you convenience, which you will need to modify for your device IDs and your power company's time-of-use plan.

Here I show a Wattvision graph of my alternating power usage during this last weekend's partial-peak time, then transition to off-peak at 8PM. If you want to see graphs like this yourself your power company may support reading your meter directly, such as the PG&E smart meter integration with the Rainforest Eagle.

Now sit back and enjoy the savings! You may need to tweak your settings to your specific house and HVAC as you go. It took me about a week's worth of experiments to dial mine in. Good luck and feel free to ask if any questions.

Step 10: CHEATING!

So now you're saving half on your energy bill each month, but you're sitting at home one hot day and you just can't bear it - you HAVE to have some more of that cool air. Just create yourself a new a scene that adds a few credits:

local id_hvac = 39
local var_credits = tonumber(luup.variable_get("TimeOfUse", "TimeOfUseCredits", id_hvac) or 0)
var_credits = var_credits + 5
luup.variable_set("TimeOfUse", "TimeOfUseCredits", var_credits, id_hvac)return true

Use with discretion!