Monitor long your HVAC system runs per day or month

Step 1: Create a template sensor that will monitor the state of a climate component

Example for generic thermostat component that drives a generic switch:

  - platform: template
    sensors:
      hvac_status:
        friendly_name: "AC status" 
        value_template: >-
          {%- if is_state('climate.central_ac', 'off') %}
            off
          {% elif is_state('climate.central_ac', 'cool')  and is_state('switch.hvac_cool_switch', 'off') %}
            idle 
          {% else %}
            on
          {%- endif %}
        icon_template: >-
          {% if is_state('sensor.hvac_status',"idle") %}
            mdi:power-on
          {% elif is_state('sensor.hvac_status',"on") %}
            mdi:snowflake
          {% else %}
            mdi:octagon
          {% endif %}

Example for a Zwave thermostat that reports state via hvac_action attribute of a climate entity

sensor:
  - platform: template
    sensors:
      linear_status:
        friendly_name: "Furnace Status" 
        value_template: >-
          {%- if is_state('climate.linear_heat', 'off') %}
            off
          {% elif is_state_attr('climate.linear_heat', 'hvac_action', 'idle') or 
                  is_state_attr('climate.linear_heat', 'hvac_action', 'fan') %}
            idle  
          {% elif is_state_attr('climate.linear_heat', 'hvac_action', 'cooling') %}
            cool 
          {% elif is_state_attr('climate.linear_heat', 'hvac_action', 'heating') %}
            heat 
          {% else %}
            unknown
          {%- endif %}
        icon_template: >-
          {% if is_state('sensor.linear_status',"idle") %}
            mdi:power-on
          {% elif is_state('sensor.linear_status',"cool") %}
            mdi:snowflake
          {% elif is_state('sensor.linear_status',"heat") %}
            mdi:fire
          {% else %}
            mdi:octagon
          {% endif %}

Step 2: Create History Stats sensors.
Example for the zwave thermostat above

  - platform: history_stats
    name: Heat ON time
    entity_id: sensor.linear_status
    state: 'heat'
    type: time
    end: '{{ now() }}' 
    duration: 
      hours: 24
      #sensor 8:
  - platform: history_stats
    name: Heat ON duty cycle 
    entity_id: sensor.linear_status
    state: 'heat'
    type: ratio
    end: '{{ now() }}' 
    duration: 
      hours: 24
  - platform: history_stats
    name: Heat ON count
    entity_id: sensor.linear_status
    state: 'heat'
    type: count
    end: '{{ now() }}' 
    duration: 
      hours: 24

Can be done to track usage month to date as well. The value for purge_keep_days in the recorder component must be greater than 31 for this to be accurate.

  - platform: history_stats
    name: Monthly Heat ON Time
    entity_id: sensor.linear_status
    state: 'heat'
    type: time
    start: '{{ now().replace(hour=0).replace(minute=0).replace(second=0).replace(day=1) }}'
    end: '{{ now() }}'
6 Likes

Thanks for sharing. I’m new to Home Assistant and examples like this help a lot! Question: What files do you add this code to? I found something that said I should add it as sensors in Configuration.yaml. Unfortunately, I get mapping values are not allowed here.

It goes into configuration.yaml in a sensors: block section. I hindsight I should have added the preceeding sensors: block section to the example. If you already have an existing sensor: block you can add it there. Not putting it into a sensor: block in give you the mapping error.

sensor:
  - platform: history_stats
    name: Monthly Heat ON Time
    entity_id: sensor.linear_status
    state: 'heat'
    type: time
    start: '{{ now().replace(hour=0).replace(minute=0).replace(second=0).replace(day=1) }}'
    end: '{{ now() }}'
1 Like

I think I have it in the correct place. When I try to use the Zwave thermostat example I get an error on the “sensors:” line after the “- platform: template”.

sensor:
  # Weather prediction
  - platform: yr
  # Start of upstairs thermostat template
  - platform: template
      sensors:
        Upstairs_AC_status:
        friendly_name: "Upstairs AC" 
        value_template: >-

Also to make sure I didn’t miss something foundational.

  • I have two thermostats

  • I created two copies of the platform template for the Zwave thermostat with operating_state attribute

  • Changed the name of both templates and the friendly names

  • Changed the climate.[*] devices to my thermostat’s climate.[name]

  • I added history_stats blocks for Heat on time and Cool on time for both thermostats in the same sensor block

    - platform: history_stats
      name: Upstairs Heat ON time
      entity_id: sensor.Downstairs_AC_status
      state: 'heat'
      type: time
      end: '{{ now() }}' 
      duration: 
       hours: 24
    - platform: history_stats
      name: Cool ON time
      entity_id: sensor.Downstairs_AC_status
      state: 'heat'
      type: time
      end: '{{ now() }}' 
      duration: 
        hours: 24
    - platform: history_stats
      name: Upstairs Heat ON time
      entity_id: sensor.Upstairs_AC_status
      state: 'heat'
      type: time
      end: '{{ now() }}' 
      duration: 
       hours: 24
    - platform: history_stats
      name: Cool ON time
      entity_id: sensor.Upstairs_AC_status
      state: 'heat'
      type: time
      end: '{{ now() }}' 
      duration: 
       hours: 24

Your problem seems to be the indenting. YAML is very unforgiving of things not being lined up right.

In the first example, the sensors: block and everything below is indented too much by 2 spaces. The “s” of sensors: has to line up below the “p” of platform: template.

In history_stats make sure the “-” in “- platform: history_stats” lines up under the “n” of sensor: on the very first line of your first example. In the hours: 24 indented below duration: they have to be indented two spaces to make the “h” go under the “r”. It is correct in the first usage but in the other two only is indented by a single space. Also some your states need to be changed from “heat” to “cool” and some of the names are mismatched with the entity_id you are intending to monitor.

In your entity_ids: Home assistant doesn’t allow capital letters in entity ids so for example all strings containing Upstairs_AC_status will have to become upstairs_ac_status.

One caveat about how this works is you also need to make sure that your Zwave thermostat is properly reporting a change in the operating_state attribute when it is calling for heating or cooling. I have a linear/go-control branded zwave thermostat purchased from Lowes and I had to go into the zwave settings for the node the thermostat was on and set the zwave configuration attribute #23 to a value of 9215 to get the thermostat to do the proper reporting because by default it did not. Hopefully your thermostat does this properly.

1 Like

just wanted to chime in on this thread to say thanks @eyager! This was my first search result for hvac monitoring and it’s super simple to implement! I have multiple zones on a single furnace/ac, so I threw together a template sensor to monitor when any of these weren’t idle and measure off of that binary sensor.

I have 2 Radio Thermostat CT50s and have been using this config to create a history stats counter to track my heating and cooling runtimes. It works great, except if I put the thermostat into “auto” mode (where it will switch between heating/cooling mode automatically). In auto mode, my runtime counter doesn’t work because the thermostat state is always “auto” and HA never knows when it is calling for heating or cooling. Any idea if the heating/cooling runtime can be tracked in auto mode?

binary_sensor:
  - platform: template
    sensors:
      hvac_downst_heat:
        friendly_name: "Downstairs Heater"
        value_template: >-
          {{ is_state_attr('climate.down_thermostat', 'hvac_action', 'heating') }}
  - platform: template
    sensors:
      hvac_downst_cool:
        friendly_name: "Downstairs A/C"
        value_template: >-
          {{ is_state_attr('climate.down_thermostat', 'hvac_action', 'cooling') }}


sensor:
  - platform: history_stats
    name: Daily DN Heating ON Time
    entity_id: binary_sensor.hvac_downst_heat
    state: 'on'
    type: time
    start: '{{ now().replace(hour=0).replace(minute=0).replace(second=0) }}'
    end: '{{ now() }}'
  - platform: history_stats
    name: Hourly DN Cooling ON Time
    entity_id: binary_sensor.hvac_downst_cool
    state: 'on'
    type: time
    start: '{{ now().replace(minute=0).replace(second=0) }}'
    end: '{{ now() }}'

Hi
Sorry, I am very new to this. Do I copy the

  • platform: template
    sensors:
    hvac_status:
    into the config.yaml?

Thanks for this, BTW!

Does the binary sensor have to exist for the duration of sampling in order to get accurate data? I just created the sensor and template (and my heating system was on this past winter), but I see 0h in the templated entity’s graph. Is that because the binary sensor was just created?

Is there a way to extract the accumulated history in the db prior to the creation of the sensor?

The history stats sensor can only get history data from the template sensor after the template sensor has been created.

Can the history in db be extracted. Technically yes. There is a SQL sensor but you’d have to create right custom query to search the states table of the database and accrue the time were the hvac_action attribute of the climate device contains the desired value. Currently the the history_stats sensor can only query by matching state and not by matching attribute.

The history stats sensor is also limited to the purge time of your database set in configuration.yaml.

recorder:  
  purge_keep_days: 45 

Thanks for your response. Still no luck.

As a simplification, I tried creating history_stats for a binary_sensor (a patio door reed switch, 7 days history), and that is also showing 0h. So I must be doing something else wrong. Here’s the simplified one:

sensor:
  - platform: history_stats
    name: Bedroom patio door seven days
    entity_id: binary_sensor.bedroom_patio_door
    state: "Open"
    type: time
    duration:
      days: 7
    end: "{{ now() }}"

Note that I don’t have recorder purge_keep_days set in my configuration.yaml, but it’s supposed to default to 10 days. (BTW is there a way to query this from the UI? The only reference to the recorder integration I could find in Lovelace was the recorder.purge service I could call).

Argh, never mind. I just realized that the state to be checked has to be “on”, not “Open” (as it appears in the UI). The simplified one is working now. Will try again with the HVAC template sensor.

I’ve had issues with recorder in the past, so I white list everything now.

If I’m understanding what is happening correctly, this is being captured in real time, so my feeling is the original sensor doesn’t need to be white listed in recorder, but that the new history_stats based sensors do?

I tried to do this with nest but I can’t find status sensor within the HA integration. All I am seeing is humidity and temp sensors.
On n’est app however I can see the HVAC run history.

You’re looking for an Attribute attached to the climate.nest entity. In Attributes (You can see these if you go look at it in Developer Tools… States… ) you’ll see a hvac_action: attribute, which is idle when Idle, and similarly cooling or heating.

1 Like

Thanks for the example. I followed it but did it based on power consumption using a Shelly EM since my HVAC doesn’t tell me when it is on.

- platform: template

    sensors:

      hvac_running:

        unique_id: "290323092023"

        value_template: >-

          {% if ( states('sensor.ac_outside_power_total') | float ) < 250 %}

            idle

          {% else %}

            on

          {%- endif %}

        icon_template: >-

          {% if is_state('sensor.hvac_running',"idle") %}

            mdi:power-on

          {% elif is_state('sensor.hvac_running',"on") %}

            mdi:snowflake

          {% else %}

            mdi:octagon

          {% endif %}

  - platform: template

    sensors:

      hvac_status:

        unique_id: "2903230920233"

        value_template: >-

          {% if ( states('sensor.ac_outside_power_total') | float ) < 250 %}

            idle

          {% elif ( states('sensor.ac_outside_power_total') | float ) < 750 %}

            low

          {% elif ( states('sensor.ac_outside_power_total') | float ) < 1500 %}

            medium

          {% else %}

            high

          {%- endif %}

        icon_template: >-

          {% if is_state('sensor.hvac_status',"idle") %}

            mdi:power-on

          {% elif is_state('sensor.hvac_status',"low") %}

            mdi:snowflake

          {% elif is_state('sensor.hvac_status',"medium") %}

            mdi:snowflake

          {% elif is_state('sensor.hvac_status',"high") %}

            mdi:snowflake

          {% else %}

            mdi:octagon

          {% endif %}

  - platform: history_stats

    name: HVAC ON time per hour

    entity_id: sensor.hvac_running

    state: "on"

    type: time

    end: "{{ now() }}"

    duration:

      hours: 2

  - platform: history_stats

    name: HVAC ON duty cycle in 2 hr

    entity_id: sensor.hvac_running

    state: "on"

    type: ratio

    end: "{{ now() }}"

    duration:

      hours: 2

  - platform: history_stats

    name: HVAC ON count per hour

    entity_id: sensor.hvac_running

    state: "on"

    type: count

    end: "{{ now() }}"

    duration:

      hours: 1

  - platform: history_stats

    name: HVAC ON duty cycle yesterday

    entity_id: sensor.hvac_running

    state: "on"

    type: time

    end: "{{ now().replace(hour=0, minute=0, second=0) }}"

    duration:

      hours: 24

  - platform: history_stats

    name: HVAC high minutes yesterday

    entity_id: sensor.hvac_status

    state: "high"

    type: time

    end: "{{ now().replace(hour=0, minute=0, second=0) }}"

    duration:

      hours: 24

Hi Guys,
I’d also appreciate if some could tak a look what is not right here.
I’m trying to count time when falownik ( solar PV) is on or working on max output.

sensor definition:

template:
  - binary_sensor:
      - name: "falownik_max_moc"
        state:  >
          {% set wartosc = states('sensor.moc_pv')|float %}        
          {{ (wartosc) >4985 }}

  - binary_sensor:
      - name: falownik
        state: >
          {% set wartosc = states('sensor.moc_pv')|float %}        
          {{ (wartosc) >0 }}

history definition:

  - platform: history_stats
    name: Falownik max
    entity_id: sensor.falownik_max_moc
    state: 'on'
    type: time
    start: '{{ now().replace(hour=0).replace(minute=0).replace(second=0) }}'
    end: '{{ now() }}' 
      
  - platform: history_stats
    name: Falownik praca
    entity_id: sensor.falownik
    state: 'on'
    type: time
    start: '{{ now().replace(hour=0).replace(minute=0).replace(second=0) }}'
    end: '{{ now() }}' 

and result… nothing is measured although one state is “on”

entity_id: binary_sensor.falownik_max_moc
entity_id: binary_sensor.falownik
1 Like

Thanks!
:man_facepalming:

Hi,
I try to use this for a netamo thermostat.

But i always have an error. The sensor is unavailable.

If someone could check my code, it will be great :

- platform: template
  sensors:
    test:
      friendly_name: test_test
      icon_template: mdi:radiator
      value_template: "{ states('sensor.duree_de_chauffe' , 'hvac_modes' , 'heat')"
      unit_of_measurement: 'min'

- platform: history_stats
  name: "duree_de_chauffe"
  entity_id: climate.salon
  state: "heat"
  type: time
  # end today at 00:00:00
  end: "{{ now().replace(hour=0, minute=0, second=0, microsecond=0) }}"
  duration:
    hours: 24

thanks per advance :slight_smile: