Heaty - a flexible heating control, facilitating schedules and manual intervention

@LarsAC Exactly that is the way which I think makes most sense. Valve positions aren’t widely available throughout vendors and thermostats.

So I’d propose the following:

  1. Add an energy_zones section to the config in which you can configure the zones and the rooms they provide with energy.
  2. A new sensor entity gets created in HA (something like sensor.heaty_<heaty_id>_<zone_name>_temp_delta), which will be updated with the maximum difference of wanted/current temperature found in the rooms of that zone.
  3. One can create HA automations to do whatever he wants in order to turn on/off the desired energy sources when the sensor’s value changes.

Names of sensors and config flags can be discussed, of course.

Any thoughts about that?

Ok, I started implementing this now, but can’t tell yet when there will be something to test. The way I’m implementing the reporting system will make it easy to add statistics of more parameters (like the valve position) in the future.

Please note this is nothing I need for myself at the moment, hence the priority isn’t too high.

The approach is sound.

I was assuming you would just add a zone field to the room definition, but the reverse would also work.

With 30 TRVs spread over 20 rooms, and across 4 zones there are a few customisation that would be of help here.

To help visualise the implementation let’s list the rooms and zones

‘’’yaml
Room
Bedroom 1
TRV 01
TRV 02
Zone Upstairs
DemandAllowed Yes
Bedroom 1 Ensuite
TRV 03
Zone Upstairs
DemandAllowed No
Bedroom 1 Walk In
TRV 04
Zone Upstairs
DemandAllowed No
Bedroom 2
TRV 05
Zone Upstairs
DemandAllowed Yes
Bedroom 2 ensuite
TRV 06
Zone Upstairs
DemandAllowed No
Bedroom 3
TRV 07
Zone Upstairs
DemandAllowed No
Office
TRV 08
TRV 09
Zone Upstairs
DemandAllowed Yes
Upstairs Landing
TRV 10
TRV 11
Zone Upstairs
DemandAllowed No
Kitchen and Sunroom
TRV 12
TRV 13
TRV 14
TRV 15
TRV 16
Zone MainA
DemandAllowed Yes
Main Hall West
TRV 17
Zone MainA
DemandAllowed No
Main Hall East
TRV 18
Zone MainB
DemandAllowed No
Girls Room
TRV 19
TRV 20
Zone MainB
DemandAllowed Yes
Sitting Room
TRV 21
TRV 22
Zone MainA
DemandAllowed Yes
Toilet
TRV 23
Zone MainA
DemandAllowed No
Bathroom
TRV 24
Zone MainB
DemandAllowed Yes
MasterBed
TRV 25
TRV 26
Zone MainA
DemandAllowed Yes
‘’’

There are more but this is enough to get a scenario planned from.

As you can see despite having many rooms, each with potential of 1 or more radiators, not every room can normally get a vote on demanding the heat source. For example I would never fire the boiler to heat an ensuite if the associated bedroom did I not call, instead I let the bedroom make the call, and if the TRV valve is open in the ensuite (which it will be as it’s cold) then it will also start to warm and due to its size normall close the valve before the bedroom has reached its target.

Now, consider the bathroom. Normally I would not allow it demand heat either as 6 days a week it’s not used. Door closed, but for argument 1 evening a week the kids have a bath and then I need Domestic Hot Water and this room warm.

Next scenario, it’s typical Irish weather and it’s raining, wife uses the radiators as drying technology for the clothes and therefore touch the override button on the spirit to indicate the radiator in question show do it’s honor and warm up. Therefore potently DemandAllowed is ignored and the valve opened despite the current ambient temperature or scheduling for a predetermined time period of lets say 60 minutes

Combined. These challenges should make the logic a touch more complex.

Question is. Does my explanation make any sense.

sorry typing on an iPhone for this
Damian

@DamianFlynn It’ll work a bit different.

You define a zone like:

zones:
  upstairs:
    rooms:
      bathroom:
      kidsroom:

Then, all thermostats in these rooms are considered for calculating temperature differences and three sensors are added to HA:

sensor.heaty_default_zone_upstairs_min_temp_delta
sensor.heaty_default_zone_upstairs_avg_temp_delta
sensor.heaty_default_zone_upstairs_max_temp_delta

If you want to weight the reportings of some thermostats differently than those of others, you can define a factor in the zone definition:

zones:
  upstairs:
    [...]
    thermostat_settings:
      climate.bathroom_thermostat:
        factor: 0.2

The delta of climate.bathroom_thermostat will then be multiplied with 0.2 before min/avg/max are calculated. A factor of 0 will ignore this thermostat completely.

This API is just a draft and subject to change, so don’t consider it stable.

And manual adjustments at the thermostats are also considered because the delta is calculated from the actual target temperature at thermostat level, not the scheduled temperature for the room.

Sounds workable.

We could try this and see. I have Half the house with Evohome and the other half with sprirt so will be easy compare.

Not using valve position feels a little alien but the logic your using makes sense when we think about it.

When set of valves opened to 25% I would use this to trigger the heat source again. So in this scenario when the delta hits a threshold we would trigger the heat, but what about turning off the heat.

One thing I notice in evo is that it almost always runs the boiler for 6 minutes then off, then waits about 4 minutes and repeat cycle until zones are working, not sure if that’s efficient to be honest

Damian

It’s up to you how you turn off the boiler again. You get normal sensor entities that are constantly being updated. Just write an automation which turns heating off when the max delta goes below a certain threshold and you should be fine. Maybe add a delay to let the value stabilize before turning things on/off unnecessarily.

And as I said, it’ll be easy to add stats about valve positions as well later…

Super.

When your ready with a build I am happy to test and feedback.

Damian

Sure, but that’s going to be a rather long story…

Hi all,

You are welcome to test the current state of the master branch. Please see the new section about zones in the docs. But be prepared to find bugs.

Best regards
Robert

EDIT:
Here is again the link to the docs for the master branch.
http://hass-apps.readthedocs.io/en/latest/

And here is the Changelog for master, which should be considered before upgrading anyway.
http://hass-apps.readthedocs.io/en/latest/apps/heaty/CHANGELOG.html

@PianSom I think it was you who once asked for a way to split schedules into smaller pieces… I then came up with the IncludeSchedule() result type, which can be used to insert schedules dynamically. However, I think you just wanted to bind a set of rules to some constraints which you didn’t want to repeat for each rule, right? If so, please have a look at the new sub-schedules feature. You are welcome to test it before it finds its way into the next release.

Here are the docs for it:
http://hass-apps.readthedocs.io/en/latest/apps/heaty/writing-schedules.html#rules-with-sub-schedules

Hi @roschi

That looks great!

But I’ve been holding off using Heaty in anger until the problem with Appdaemon3 gets resolved.

@PianSom The bug has been introduced in AD 3.0.0 final, hence you can use 3.0.0b5 w/o any problems.

However, since my PR hasn’t been merged into AD yet, I decided to no longer depend on the broken method for now, so the current state of the master branch also works with 3.0.0+.

Maybe you want to try it.

Best regards
Robert

1 Like

@roschi
Hello Robert

I have dusted off my Heaty installation and it seems to have installed the latest version. You may recall that (unfortunately) I installed Heaty without using a virtualenv. On the upside, I am running AD on a different machine from HA, one which is used for largely unchanging network infrastructure purposes, so hopefully this won’t matter too much.

I had previously disabled Heaty by moving my .yaml file to a “.hidden” extension, so moved it back and happily it AD recognised it, and reported

2018-06-12 10:08:58.287859 INFO AppDaemon: /home/pi/appdaemon/apps/04_heaty/heaty.yaml added or modified
2018-06-12 10:08:58.289137 INFO AppDaemon: App 'heaty_full' added
2018-06-12 10:08:58.293785 INFO AppDaemon: Initializing app heaty_full using class HeatyApp from module hass_apps_loader
2018-06-12 10:08:58.548372 INFO heaty_full: --- heaty v0.9.4 initialization started

Less happily, the AD error log showed the following:

2018-06-12 10:08:58.619588 WARNING AppDaemon: ------------------------------------------------------------
2018-06-12 10:08:58.620281 WARNING AppDaemon: Unexpected error running initialize() for heaty_full
2018-06-12 10:08:58.620894 WARNING AppDaemon: ------------------------------------------------------------
2018-06-12 10:08:58.635003 WARNING AppDaemon: Traceback (most recent call last):
  File "/home/pi/.local/lib/python3.5/site-packages/appdaemon/appdaemon.py", line 1513, in init_object
    init()
  File "/usr/local/lib/python3.5/dist-packages/hass_apps/common.py", line 44, in initialize
    self.cfg = self.Meta.config_schema(self.args)
  File "/home/pi/.local/lib/python3.5/site-packages/voluptuous/schema_builder.py", line 267, in __call__
    return self._compiled([], data)
  File "/home/pi/.local/lib/python3.5/site-packages/voluptuous/validators.py", line 204, in _run
    return self._exec(self._compiled, value, path)
  File "/home/pi/.local/lib/python3.5/site-packages/voluptuous/validators.py", line 284, in _exec
    raise e if self.msg is None else AllInvalid(self.msg)
  File "/home/pi/.local/lib/python3.5/site-packages/voluptuous/validators.py", line 282, in _exec
    v = func(path, v)
  File "/home/pi/.local/lib/python3.5/site-packages/voluptuous/schema_builder.py", line 769, in validate_callable
    return schema(data)
  File "/home/pi/.local/lib/python3.5/site-packages/voluptuous/schema_builder.py", line 267, in __call__
    return self._compiled([], data)
  File "/home/pi/.local/lib/python3.5/site-packages/voluptuous/schema_builder.py", line 587, in validate_dict
    return base_validate(path, iteritems(data), out)
  File "/home/pi/.local/lib/python3.5/site-packages/voluptuous/schema_builder.py", line 425, in validate_mapping
    raise er.MultipleInvalid(errors)
voluptuous.error.MultipleInvalid: expected a list for dictionary value @ data['schedule_append']

My heaty.yaml is appended below, for info. But I have not changed it for quite a while.

My suspicion is that your changes have eliminated the need for (and therefore presence of) IncludeSchedule - is that right? If so I will re-write my .yaml file and try again - but just wanted to check first :slight_smile:

PS Please forgive me, but I have forgotten where we got to with Heaty controlling a “Hot Water” timer. My recollection is that because my Hive system has three modes for Hot Water - Auto, On, Off - which are diffreent for the modes for my heating circuits, it was not so easy to incorporate within Heaty. Is that right?

heaty_full:
  module: hass_apps_loader
  class: HeatyApp

#  master_switch: input_boolean.heating_master

  thermostat_defaults:
    set_temp_retries: 10
    set_temp_retry_interval: 60

    temp_service: climate/set_temperature
    opmode_heat: "heat"
    opmode_off: "off"

  schedule_snippets:
    downstairs_winter_default:
      - { weekdays: 1-5, start: "05:00", end: "06:00", temp: "23 if app.get_state('sensor.temperature__outside_front_hall') <= '-2' else Ignore()" }   # Boost if chilly now
      - { weekdays: 1-5, start: "06:00", end: "08:30", temp:  23   }
      - { weekdays: 1-5, start: "08:30", end: "16:30", temp: "23 if app.get_state('group.someone_is_home') == 'on' else 21" }                          # Boost if someone at home
      - { weekdays: 1-5, start: "16:30", end: "22:00", temp:  23   }

      - { weekdays: 6,   start: "05:00", end: "06:00", temp: "23 if app.get_state('sensor.temperature__outside_front_hall') <= '-2' else Ignore()" }   # Boost if chilly now
      - { weekdays: 6,   start: "06:00", end: "10:30", temp:  23   }
      - { weekdays: 6,   start: "08:30", end: "16:30", temp: "23 if app.get_state('group.someone_is_home') == 'on' else 21" }                          # Boost if someone  at home
      - { weekdays: 6,   start: "16:30", end: "22:00", temp:  23   }

      - { weekdays: 7,   start: "05:00", end: "06:00", temp: "23 if app.get_state('sensor.temperature__outside_front_hall') <= '-2' else Ignore()" }   # Boost if chilly now
      - { weekdays: 7,   start: "06:00", end: "22:00", temp:  23   }

      - {                                              temp: "20 if app.get_state('sensor.pws_weather_1n_metric') <= '-2' else 18"  }                  # Boost if probably chilly tonight

    downstairs_shoulder_default:
      - { weekdays: 1-5, start: "06:30", end: "07:30", temp:  22   }
      - { weekdays: 1-5, start: "07:30", end: "16:30", temp: "22 if app.get_state('input_boolean.janeathome') == 'on' else 20" }                       # Boost if wife at home
      - { weekdays: 1-5, start: "16:30", end: "22:00", temp:  23   }

      - { weekdays: 6,   start: "06:30", end: "09:00", temp:  22   }
      - { weekdays: 6,   start: "09:00", end: "16:30", temp: "22 if app.get_state('group.someone_is_home') == 'on' else 20" }                          # Boost if someone at home
      - { weekdays: 6,   start: "16:30", end: "22:00", temp:  23   }

      - { weekdays: 7,   start: "06:30", end: "22:00", temp:  22   }

      - {                                              temp:  17   }

    downstairs_summer_default:
      - {                                              temp:  15   }



    upstairs_winter_default:
      - { weekdays: 1-5, start: "05:00", end: "08:30", temp:  20.5 }
      - { weekdays: 1-5, start: "16:30", end: "21:30", temp:  21   }

      - { weekdays: 6-7, start: "05:00", end: "06:00", temp: "21 if app.get_state('sensor.temperature__outside_front_hall') <= '-2' else Ignore()" }   # Boost if chilly now
      - { weekdays: 6-7, start: "06:00", end: "10:00", temp:  21   }
      - { weekdays: 6-7, start: "16:30", end: "22:00", temp:  21   }

      - {                                              temp:  18.5 }

    upstairs_shoulder_default:
      - { weekdays: 1-5, start: "05:00", end: "08:00", temp:  20.5 }
      - { weekdays: 1-5, start: "18:00", end: "21:30", temp:  21   }

      - { weekdays: 6-7, start: "06:00", end: "10:00", temp:  21   }
      - { weekdays: 6-7, start: "16:30", end: "22:00", temp:  21   }

      - {                                              temp:  18.5 }

    upstairs_summer_default:
      - {                                              temp:  15   }


    hotwater_winter_default:
      - { weekdays: 1-5, start: "05:45", end: "08:00", temp:  0    }
      - { weekdays: 1-5, start: "12:00", end: "12:30", temp:  0    }
      - { weekdays: 1-5, start: "16:30", end: "20:30", temp:  0    }

      - { weekdays: 6-7, start: "05:45", end: "10:00", temp:  0    }
      - { weekdays: 6-7, start: "12:00", end: "12:30", temp:  0    }
      - { weekdays: 6-7, start: "16:00", end: "20:30", temp:  0    }
      - { OFF }

    hotwater_shoulder_default:
      - { weekdays: 1-5, start: "05:00", end: "08:00", temp:  0    }
      - { weekdays: 1-5, start: "18:00", end: "21:30", temp:  0    }

      - { weekdays: 6-7, start: "06:00", end: "10:00", temp:  0    }
      - { weekdays: 6-7, start: "16:30", end: "22:00", temp:  0    }
      - { OFF }

    hotwater_summer_default:
      - { weekdays: 1-5, start: "05:00", end: "08:00", temp:  0    }
      - { weekdays: 1-5, start: "18:00", end: "21:30", temp:  0    }

      - { weekdays: 6-7, start: "06:00", end: "10:00", temp:  0    }
      - { weekdays: 6-7, start: "16:30", end: "22:00", temp:  0    }
      - { OFF }


  rooms:
    downstairs:
      friendly_name: Downstairs
#      thermostats:
#        climate.downstairs_heating:
      schedule:
      - start_date: { month: 1,  day: 1  }
        end_date:   { month: 3,  day: 15 }
        temp: IncludeSchedule(schedule_snippets["downstairs_winter_default"])

      - start_date: { month: 3,  day: 31 }
        end_date:   { month: 5,  day: 30 }
        temp: IncludeSchedule(schedule_snippets["downstairs_shoulder_default"])

      - start_date: { month: 6,  day: 1  }
        end_date:   { month: 9,  day: 15 }
        temp: IncludeSchedule(schedule_snippets["downstairs_summer_default"])

      - start_date: { month: 9,  day: 16 }
        end_date:   { month: 10, day: 30 }
        temp: IncludeSchedule(schedule_snippets["downstairs_shoulder_default"])

      - start_date: { month: 11, day: 1  }
        end_date:   { month: 12, day: 31 }
        temp: IncludeSchedule(schedule_snippets["downstairs_winter_default"])


      friendly_name: Upstairs
#      thermostats:
#        climate.upstairs_heating:
      schedule:
      - start_date: { month: 1,  day: 1  }
        end_date:   { month: 3,  day: 15 }
        temp: IncludeSchedule(schedule_snippets["upstairs_winter_default"])

      - start_date: { month: 3,  day: 31 }
        end_date:   { month: 5,  day: 30 }
        temp: IncludeSchedule(schedule_snippets["upstairs_shoulder_default"])

      - start_date: { month: 6,  day: 1  }
        end_date:   { month: 9,  day: 15 }
        temp: IncludeSchedule(schedule_snippets["upstairs_summer_default"])

      - start_date: { month: 9,  day: 16 }
        end_date:   { month: 10, day: 30 }
        temp: IncludeSchedule(schedule_snippets["upstairs_shoulder_default"])

      - start_date: { month: 11, day: 1  }
        end_date:   { month: 12, day: 31 }
        temp: IncludeSchedule(schedule_snippets["upstairs_winter_default"])


      friendly_name: Hot water
#      thermostats:
#        climate.hot_water:
#          opmode_heat: "on"
      schedule:
      - start_date: { month: 1,  day: 1  }
        end_date:   { month: 3,  day: 15 }
        temp: IncludeSchedule(schedule_snippets["hotwater_winter_default"])

      - start_date: { month: 3,  day: 31 }
        end_date:   { month: 5,  day: 30 }
        temp: IncludeSchedule(schedule_snippets["hotwater_shoulder_default"])

      - start_date: { month: 6,  day: 1  }
        end_date:   { month: 9,  day: 15 }
        temp: IncludeSchedule(schedule_snippets["hotwater_summer_default"])

      - start_date: { month: 9,  day: 16 }
        end_date:   { month: 10, day: 30 }
        temp: IncludeSchedule(schedule_snippets["hotwater_shoulder_default"])

      - start_date: { month: 11, day: 1  }
        end_date:   { month: 12, day: 31 }
        temp: IncludeSchedule(schedule_snippets["hotwater_winter_default"])

@PianSom 0.9.4 is not the latest version… You need to install the state of the master branch. The best would be to uninstall the whole thing and follow the steps on the Getting Started page. Choose to install from GitHub (variant b).

IncludeSchedule is still there. Everything that happened is mentioned in the Changelog for Heaty, which can be found in the docs and which I linked above.

Regarding the hot water… http://hass-apps.readthedocs.io/en/latest/apps/heaty/index.html#what-can-heaty-do-and-what-not

Best regards
Robert

Hello,

exist user interface for settings schedule? For example

You asked this before. The answers given then still apply …

1 Like

I added this to the docs now under 7.

http://hass-apps.readthedocs.io/en/latest/apps/heaty/index.html#what-can-heaty-do-and-what-not

I have somethign like that in HA myself. It is utilitizing Heaty just partialy - the temperature values are evaluated from inputs and as such it not working fully automatically.
Every 10 minute Heaty reschedule must be called as automation. So you can see the schedule has 10 mintues granularity. Also the open winodws are correctly catched by Heaty, that’s for me the biggest advantage as I do not have to create overcomplicated automations.
In general it is already working but as heating season is still few months ahead it is not yet finalized and ready to share :slight_smile:

Bellow code is exampel of Heaty configuration (not yet taking into account the on-off swithc in GUI):

      # Work day, first interval of high temperature (set start 0:00 to ignore interval)
      - temp: app.get_state("input_number.bedroom_high_temperature") if (int(app.get_state("sensor.bedroom_work_first_on")) > 0) and (app.get_state("binary_sensor.workda
y_sensor") == "on") and ((time.hour*60+time.minute) >= int(app.get_state("sensor.bedroom_work_first_on"))) and ((time.hour*60+time.minute+1) <= int(app.get_state("sensor
.bedroom_work_first_off"))) else Ignore()

      # Work day, second interval of high temperature (set start 0:00 to ignore interval)
      - temp: app.get_state("input_number.bedroom_high_temperature") if (int(app.get_state("sensor.bedroom_work_second_on")) > 0) and (app.get_state("sensor.workday_sen
sor") == "on") and ((time.hour*60+time.minute) >= int(app.get_state("sensor.bedroom_work_second_on"))) and ((time.hour*60+time.minute+1) <= int(app.get_state("sensor.bed
room_work_second_off"))) else Ignore()

      # At all other times, set temperature to low temperature
      - temp: app.get_state("input_number.bedroom_low_temperature")