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

Hi @roschi

Great, that sounds like it will be useful (if I had a properly working heating system).

I have not done further work on my apps.yaml, but have been thinking further about how best to approach what I want with heaty. The posts above will be great as a basis for my presence needs, though it will need more customisation over time. I’m a little unsure how to change schedule snippets when the season changes.

At the moment I have

  rooms:
    downstairs:
      friendly_name: Downstairs
#      thermostats:
#        climate.downstairs_heating:
      schedule:
      - temp: IncludeSchedule(schedule_snippets["downstairs_winter_default"])

Will this change to something like:

      - temp: { months: 10-3, IncludeSchedule(schedule_snippets["downstairs_winter_default"]) }

(I’m quite certain that is wrong, but can’t quite see what it should be!)

Moving on, I am thinking now about what I would like to happen to my schedule when it is ambient cold.

What I am thinking is a “rule” of the following type: if the weather forecast for tonight shows an expected temperature of -2C or below then adopt a schedule downstairs_winter_warmer. I envisage that other schedule to be similar to my default schedule, though with a higher night-time thermostat setting, and an earlier start on the morning setting - say 5am start rather than 6am.

What do you think? Am I better off as I am envisaging things? How would I trigger a switch in schedules - via a HA automation? Or will I need to delve in to a Python external module?

Or might it be “better” to try to adjust the existing schedule with rules? Is it even possible to have a start time which is an in-line function?

Sorry for all the newbie questions. I really have read readme.rst!

  1. months: 10-3 won’t work, because the range start has to be lower than the range end. Either do months: 1-3,10-12 or use the new start_date and end_date, omitting the year field.

  2. Constraints like months: and start_date: don’t go inside the temp: value, they have to be on the same level as start: and end:.

  3. If you haven’t done already, create a sensor in Home Assistant that shows the weather forecast temperature you are interested in.

  4. start: and end: can just be fixed times, no expressions. Just insert another rule for the time from 5 am to 6 am into your existing winter_default schedule which is dependent upon the weather forecast sensor’s value. The fallback temperature for tonight can also be made dependent on this sensor, so you don’t create two nearly identical schedules.

  5. Turning a switch from inside a temperature expression is not advised, as you don’t know when and how often a schedule rule is evaluated. Just query state from inside a temperature expression, don’t change it from there.

Regarding points 1 and 2, there are actual working examples in the README that show how to use the constraints.

OK, this is more of a proof-of-concept than a working example (I haven’t yet updated to include your start_date: and end_date: version, so am still getting errors, and I’ve kept some of your comments for things I want to come back to), but here is my apps.yaml excerpt right now =

heaty_full:
  module: hass_apps_loader
  class: HeatyApp

  # This switch can be used to turn off all rooms (e.g. for vacation times).
  # You may use any switch that has the states "on" and "off".
  #master_switch: input_boolean.heating_master

  thermostat_defaults:
    set_temp_retries: 10
    set_temp_retry_interval: 60

    opmode_heat: "heat"
    opmode_off: "off"

    # You might want to use an alternative service that receives
    # the operation mode value.
    # (optional, default: climate/set_operation_mode)
    #opmode_service: climate/set_operation_mode
    # (optional, default: operation_mode)
    #opmode_service_attr: operation_mode
    # Entity attribute that holds the current operation mode.
    # This is used to detect manual temperature adjustments, provide
    # correct temperature values on startup and notice that a
    # thermostat picked up temperature changes.
    # (optional, default: operation_mode)
    #opmode_state_attr: operation_mode

    # You might want to use an alternative service that receives
    # the temperature value.
    # (optional, default: climate/set_temperature)
    temp_service: climate/set_temperature
    # (optional, default: temperature)
    #temp_service_attr: temperature
    # Entity attribute that holds the current target temperature.
    # This is used to detect manual temperature adjustments, provide
    # correct temperature values on startup and notice that a
    # thermostat picked up temperature changes.
    # (optional, default: temperature)
    #temp_state_attr: temperature

  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('input_boolean.janeathome') == 'on' else 21" }                       # Boost if wife 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('input_boolean.janeathome') == 'on' else 21" }                       # Boost if wife 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('input_boolean.janeathome') == 'on' else 20" }                       # Boost if wife 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 }



  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: 16 }
        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:

#      friendly_name: Hot water
#      thermostats:
#        climate.hot_water:
#      schedule:
1 Like

Yes, from the first glance, it should work as expected, although with a bit of overhead.

And on May 31st you have no schedule :slight_smile:

Oops! :slight_smile:

Next job is to introduce some inline programming for temp: to choose a different schedule by season if we are (a) home or about to come home (the latter may have to be a manual switch) (ie downstairs_home_default) (b) away for a relatively short period or (downstairs_home_snooze) © away for a long period (downstairs_home_sleep).

And then, of course, to do the analogous for upstairs and hot water.

Long way to go …

I might tend to repeat myself… but why do you introduce that many different schedules? Maintaining such a setup must be horribly complicated.

What about (just pseudocode, you need to insert actual sensor value queries):

- temp: Add(-2) if "nobody at home" else Ignore()

Or, to make throttling dependent on the distance you are away:

- temp: Add(-min(3, math.floor(distance_of_nearest_inhabitant_in_km / 10) / 2))

which would give 0.5 degrees per 10 km with a maximum of 3 degrees. Consider adding such a rule right before the schedule selection? That way it would count for every schedule. Or, if desired, add the rule to the beginning of the winter_default schedule only.

I guess that having three different schedules is conceptually what I do at the moment, albeit manually. And I think I’ll find it easier to maintain and tweak a schedule if it’s in a readable form like the one I have done already, rather than a set of rules that need to be worked through to determine a target temperature.

I can’t just add -2C if no-one is home (even though I already have that sensor set up) because I live in a large, old, drafty house, So it cools down quite quickly and heats up slowly. I sometimes stay here, and sometimes (unpredictably) in another house. The issue is that I don’t want the heating to step down if I’m here but out for a few hours at the shops or whatever.

However I do want the heating semi-dormant if I’m at my other place (because of the large thermal mass of this place it takes ages to heat from scratch), and completely dormant if I’m out of the country or not here for a long time. Semi-dormancy will mean keeping a moderate base temperature with an up-tick towards late afternoon (the time I typically come here if I am returning, so that if I forget to tell the heating I am coming then it’s not freezing when I arrive). Dormancy will be basic frost protection.

Perhaps my attitude will change after using heaty for a while, and I’ll find that a rule-based system is less work than the schedule-switching I am envisaging. But right now even though I can see how I could combine my three existing schedules in to one I really think that doing so would mean decreased readability.

However, I remain open to being persuaded of my folly :slight_smile:

Sorry if I insisted to hard on combining the schedules, but as the author of Heaty I always tend to utilize even the smallest simplifications where possible. :slight_smile:

What do you think about the second rule I proposed? The one that throttles temperature based on your distance seems quite well-suited for being out for shopping. Of course you can always tweak the values, it doesn’t have to be 0.5 degrees per 10 km.

Then you could insert another rule before which sets an even lower temperature when you are at your other place and are not going to return soon.

Please don’t apologise! I think that this is a super app and I’m really hoping it will make my life much simpler (and warmer!). And your excellent suggestions are taken as an master instructing a novice, with all due humility on my part :slight_smile:

I like the idea of distance-based heating in principle. But there are two issues - firstly, I am struggling enough with presence (with a mixture of router, bluetooth and ios at the moment) to have any confidence on reliably determining distance. Moreover, my wife regards even my current attempt to note her presence in the house as a gross and unwarranted invasion of personal privacy! And secondly, the distance I am from the house is not a good predictor of (a) whether I will return or (b) how long it will take me if I do (a relatively short distance into London can take a long time to return using public transport; a relatively long distance away out of town can mean a short hop in a car).

I suspect in the medium term I will try and work an algorithm akin to the non-binary presence I mentioned before, which ties my heating schedule to my presence state (present, away for a short while, away in second house for a few days, away for a long time). Sadly, I can’t yet see any way of telling my home automation that I expect to be home in x hours other than a manual switch.

One day Skynet will be there to take care of all of this. In the mean time I’ll make do with basic heaty schedules and Occusim. :slight_smile:

Alright, thanks for the great hymn of praise! :slight_smile:
I really love to see someone using Heaty and sharing his thoughts about it.

What about an input_number you set to the number of hours you are going to be away, and then decrease this every hour by 1 with a HA timer+automation. Then, add just a single schedule rule to subtract a fixed amount of degrees per hour infront of your schedule.

I’ve got such a setup here… don’t ask me why I didn’t think about it earlier. Configuration snippets will come in a minute :slight_smile:

Ok, here it is.

Home Assistant configuration:

input_number:
  absence_time: { min: 0, max: 24, step: 0.5, mode: box, unit_of_measurement: h }

timer:
  decrease_absence_time:

automation:
- alias: initialize decrease absence time timer
  trigger:
  - platform: state
    entity_id: input_number.absence_time
  condition:
  - condition: numeric_state
    entity_id: input_number.absence_time
    above: 0
  action:
  - service: timer.start
    data:
      entity_id: timer.decrease_absence_time
      duration: "0:30:00"

- alias: decrease absence time
  trigger:
  - platform: event
    event_type: timer.finished
    event_data: { entity_id: timer.decrease_absence_time }
  condition:
  - condition: numeric_state
    entity_id: input_number.absence_time
    above: 0
  action:
  - service: input_number.set_value
    data_template:
      entity_id: input_number.absence_time
      value: "{{ states('input_number.absence_time') | float - 0.5 }}"

The first one starts the timer whenever the absence time value changes to something above 0, the second one decreases absence time by when the timer finishes, which will cause the first automation to run again.

Then just fire a heaty_reschedule event whenever input_number.absence_time changes and reference its value in your schedule.

You could set input_number.absence_time by an automation when you leave the house in the morning, for instance, possibilities are of course endless.

Nice!

The big problem for me with that count-down: I often (usually) have no reliable idea when I’ll be back when I leave - my plans change, I get distracted, the weather happens, …

But I’ve been thinking about something similar based on expected return time - having a HA switch with a slider for x in “I expect to be home in x hours”, with a heaty event to change to my default schedule at n hours before my arrival, where n is a function of ambient temp, time absent (so n=24 if it’s cold and I’ve been away for ages; n=?3 if it’s warm and a short absence, etc). (I wonder if it’s possible to show the time remaining on the gui with CustomUI or something?)

The difference between your approach and mine is that I’d prefer to tell my system my plans when I know them, rather than when I leave.

We are actually talking about almost the same, I think. You can always adjust the input_number, which is represented as a slider, even after you left or if your plans changed. The two automations will take care of the rest. The slider doesn’t show the total time you have been away, it rather decreases every 30 minutes until it reaches 0.

And, on the Heaty side, I’d suggest creating a custom function which calculates the temperature to add to your schedule. This function may then take weather, expected return time etc. into account and return Add(x).

Here is just a little python function you might want to use from inside a schedule rule…

import math

from hass_apps.heaty import expr


def add_absence_throttling(app):
    remaining_hours = float(app.get_state("input_number.absence_time") or 0)
    outside_temp = float(app.get_state("sensor.outside_temperature") or 0)

    # Throttle 0.2 °C per hour.
    sub = remaining_hours / 5

    # Negate the effect of throttling when it's below 5 °C.
    # 0.25 °C are removed from the throttle value for each degree it is
    # below 5 °C.
    sub -= max(0, (5 - outside_temp) / 4)

    # Limit throttling to max 5 °C.
    sub = max(0, min(5, sub))

    # Align to 0.5 °C steps to avoid changing temperature too often.
    sub = math.floor(2 * sub) / 2

    return expr.Add(-sub)

Yes, now I think about it we are pretty much thinking about the same thing.

Thanks for the code. Hopefully my plumber will sort me out next week and I’ll be able to start using Heaty in anger.

In the mean time I have just upgraded Heaty to make sure my current schedule (above) works with the start_date: etc but I am getting the following error. I can’t see anything obvious in my syntax, or any reason why it should complain about the end_date: rather than the start_date: Any suggestions?

2018-02-04 13:10:37.591979 WARNING parser says
2018-02-04 13:10:37.592801 WARNING   in "<unicode string>", line 194, column 28:
            end_date:   ( month: 3,  day: 15 }
                               ^
2018-02-04 13:10:37.593304 WARNING mapping values are not allowed here

DER! Wrong bracket!

Yeah, that was it :slight_smile:

I’ve just edited the code above, there was something wrong with the use of min/max… Should work as expected now.

I installed Heaty because it looks like the most sophisticated and customisable heat control app available, quickly adapted the ‘minimal’ example configuration with my own entity_ids, then fired it up - but no joy :

2018-02-05 15:40:04.129524 WARNING AppDaemon: Traceback (most recent call last):
  File "/srv/homeassistant/lib/python3.5/site-packages/appdaemon/appdaemon.py", line 1651, in read_app
    self.init_object(name, class_name, module_name, self.app_config)
  File "/srv/homeassistant/lib/python3.5/site-packages/appdaemon/appdaemon.py", line 1446, in init_object
    self.objects[name]["object"].initialize()
  File "/srv/homeassistant/lib/python3.5/site-packages/hass_apps/common.py", line 46, in initialize
    self.initialize_inner()
  File "/srv/homeassistant/lib/python3.5/site-packages/hass_apps/heaty/app.py", line 24, in _new_func
    result = func(self, *args, **kwargs)
  File "/srv/homeassistant/lib/python3.5/site-packages/hass_apps/heaty/app.py", line 85, in initialize_inner
    state = self.get_state(therm_name, attribute="all")
  File "/srv/homeassistant/lib/python3.5/site-packages/appdaemon/appapi.py", line 211, in get_state
    return self.AD.get_state(namespace, device, entity, attribute)
  File "/srv/homeassistant/lib/python3.5/site-packages/appdaemon/appdaemon.py", line 620, in get_state
    return deepcopy(self.state[namespace])
KeyError: 'climate.living'

Running latest Hass on Pi, and latest AD 3 beta - the latter presumably being the problem. Or not ?

Hi @pav

Heaty doesn’t work with appdaemon 3 yet, which is also specified in requirements in the setup.py.

Please install again like so, then it should fetch the correct appdaemon version:

pip3 install --upgrade hass_apps