A highly-configurable, comfortable to use Home Assistant / appdaemon app that controls thermostats based on a schedule while still facilitating manual intervention at any time.
Note: Heaty is still a young piece of software which likely contains some bugs. Please keep that in mind when using it. Bug reports and suggestions are always welcome. Use the GitHub Issues for this sort of feedback.
Install from PyPi.
pip3 install hass-heaty
Or clone the GitHub repository to get even the latest changes:
git clone https://github.com/efficiosoft/hass-heaty cd hass-heaty pip3 install . --upgrade
As far as I know, it's not possible to create a plug & play add-on for hass.io containing Heaty, because it needs to be installed into AppDaemon's container.
Even though it's untested, the only actions needed in order to install under hass.io are:
- Install the appdaemon add-on.
- Copy the
hass_heaty
folder and the fileheaty_app.py
into theapps
directory of your AppDaemon container. This is also the only thing you need to do when upgrading to a newer version of Heaty. - Continue with the configuration as normal.
- Get yourself a nice cup of coffee or tea. You'll surely need it.
- Copy the file
heaty_app.py
to your AppDaemon'sapps
directory. This is just a stub that imports the real app's code, making later upgrades a little easier. - Copy the contents of
apps.yaml.example
to yourapps.yaml
file and adapt it as necessary. The example file also contains documentation comments explaining what the different settings mean. There are both a minimal and a full configuration example in that file. You'll probably want to get up and running with the minimal one and extend your configuration later, since there is really a lot you can do if you want. But don't worry, the minimal configuration will probably do just fine for now. - AppDaemon should have noticed the changes made to
apps.yaml
and restart its apps automatically.
You're done!
Simply pull upgrades from PyPi:
pip3 install --upgrade hass-heaty
Or, if you installed from the git repository:
cd /path/to/your/clone/of/the/repository git pull pip3 install . --upgrade
Note that AppDaemon doesn't detect changes in the imported modules automatically and needs to be restarted manually after an upgrade.
When upgrading from v0.2.0, please do also upgrade heaty_app.py
.
A schedule controls the temperature in a room over time. It consists of a set of rules.
Each rule must define a temperature:
schedule: - temp: 16
This schedule would just always set the temperature to 16
degrees, nothing else. Of course, schedules wouldn't make a lot
sense if they couldn't do more than that.
Here is another one:
schedule: - temp: 21.5 start: "07:00" end: "22:00" - temp: 16
This schedule contains the same rule as the schedule before, but
additionally, it got a new one. The new rule overwrites the other
and will set a temperature of 21.5
degrees, but only from 7.00 am
to 10.00 pm. This is because it's placed before the 16
-degrees-rule.
That is how Heaty schedules work. The first matching rule wins.
If you omit the start
parameter, Heaty assumes that you mean
midnight (00:00
) and fills that in for you.
When end
is not specified, Heaty does two things. First, it sets
00:00
as value for end
. This alone wouldn't make sense,
because the resulting rule would stop being valid before it started.
To achieve the behaviour we'd expect, Heaty sets another attribute,
end_plus_days: 1
. This means that the rule is valid up to the
time specified in the end
field, but one day later than the
start. Cool, right?
Having done the same manually would result in the following schedule, which behaves exactly like the previous one.
schedule: - { temp: 21.5, start: "07:00", end: "22:00" } - { temp: 16, start: "00:00", end: "00:00", end_plus_days: 1 }
Note how each rule has been rewritten to take just a single line. This is no special feature of Heaty, it's rather normal YAML. But writing rules this way is often more readable, especially if you need to create multiple similar ones which, for instance, only differ in weekdays, time or temperature.
Now we have covered the basics, but we can't create schedules based on, for instance, the days of the week. Let's do that next.
schedule: - temp: 22 weekdays: 1-5 start: "07:00" end: "22:00" - temp: 22 weekdays: 6,7 start: "07:45" - temp: 15
With your knowledge so far, this should be self-explanatory. The only
new parameter is weekdays
, which is a so called constraint.
Constraints can be used to limit the days on which the rule is considered. There are a number of these constraints, namely:
years
: limit the years (e.g.years: 2016 - 2018
months
: limit based on months of the year (e.g.months: 1-3, 10-12
for Jan, Feb, Mar, Oct, Nov and Dec)days
: limit based on days of the month (e.g.days: 1-15, 22
for the first half of the month + the 22nd)weeks
: limit based on the weeks of the yearweekdays
: limit based on the days of the week, from 1 (Monday) to 7 (Sunday)
The format used to specify values for constraints is as follows. We call it range strings, and only integers are supported, no decimal values.
x-y
: range of numbers fromx
toy
, includingx
andy
a,b
: numbersa
andb
a,b,x-y
: the previous two together- ... and so on
- Any spaces are ignored.
All constraints you define need to be fulfilled for the rule to match.
With this knowledge, writing quite powerful Heaty schedules should be easy and quick.
The next chapter deals with temperature expressions, which finally give you the power to do whatever you can do with Python, right inside your schedules.
Heaty accepts so called temperature expressions in schedules or when
manually setting a temperature via the heaty_set_temp
event.
Temperature expressions are a powerful way of expressing a temperature in relation to anything you can think of. This power comes from the fact that temperature expressions are just normal Python expressions which are evaluated at runtime. When Heaty parses its configuration, all temperature expressions are pre-compiled to make their later evaluation more performant.
Temperature expressions must evaluate to an object of type
ResultBase
. However, you should always return one of its sub-types.
Such an object can be created like Result(19)
or Result(OFF)
.
If your expression evaluates to an int
, float
or str
type,
Heaty converts it to a Result
automatically for convenience.
An object of one of the following sub-types of ResultBase
can be
returned to influence the way your result is treated.
Add(value)
, which causesvalue
to be added to the result of a consequent rule. This is continued until a rule evaluates to a finalResult
.Break()
, which causes schedule lookup to be aborted immediately. The temperature will not be changed in this case.Ignore()
, which causes the rule to be treated as if it doesn't exist at all. If one exists, the next rule is evaluated in this case.Result(value)
: just the final result which will be used as the temperature. Schedule lookup is aborted at this point.
If you want to turn the thermostats in a room off, there is a special
value available under the name OFF
. Just return that.
There is an object available under the name app
which represents
the appdaemon.appapi.AppDaemon
object of Heaty. You could,
for instance, retrieve values of input sliders via the normal
AppDaemon API.
Beside the return types like Add
, Break
, Ignore
and
Result
, the following global variables are available inside
time expressions:
app
: the appdaemon.appapi.AppDaemon objectroom_name
: the name of the room the expression is evaluated for as configured in Heaty's configuration (not the friendly name)now
: adatetime.datetime
object containing the current date and timedate
: a shortcut fornow.date()
time
: a shortcut fornow.time()
datetime
: Python'sdatetime
module
You can easily make your own code available inside temperature expressions by importing custom modules. Modules that should be available in your expressions have to be specified in the configuration like so:
temp_expression_modules: math: time: as: _time my_custom_module:
This will make the modules math
and time
from Python's standard
library available, as well as my_custom_module
. However, the
time
module will be made accessible under the name _time
to
prevent the variable time
, which is included by Heaty anyway, from
being overwritten.
Example: Use of an external module ~~~~~~~===========================
Imagine you have a module which makes some more complex decisions
based on the current state. We call it my_mod
. This could look
as follows:
# This module gives us access to Ignore as well as all other # ResultBase sub-types and OFF. from hass_heaty import expr def get_temp(room_name, app): if room_name == "bath": if app.get_state("switch.take_a_bath") == "on": return 22 return expr.Ignore()
Save the code as my_mod.py
somewhere Python can find it.
The easiest way is to store it inside AppDaemon's apps
directory.
Add the module to your temp_expression_modules
config as
explained before.
Now, we write two new schedule rules for the bath room (note their order):
schedule: - temp: my_mod.get_temp(room_name, app) - temp: 19
Last step is to write a simple Home Assistant automation to emit a
re-schedule event whenever the state of switch.take_a_bath
changes.
- alias: "Re-schedule when switch.take_a_bath is toggled" trigger: - platform: state entity_id: switch.take_a_bath action: - event: heaty_reschedule event_data: room_name: bath
We're done! Now, whenever we toggle the take_a_bath
switch, the
schedules are re-evaluated and our first schedule rule executes.
The rule invokes our custom function, passing to it the room's name
and the appdaemon.appapi.AppDaemon
object. Our custom function
checks the state of the take_a_bath
switch and, if it's enabled,
causes the temperature to be set to 22 degrees. However, if the switch
is off or we called it for a room it actually has no clue about,
the rule is ignored completely.
If that happens, the second rule is processed, which always evaluates to 19 degrees.
You should be able to extend the get_temp
function to include
functionality for other rooms now as well.
This example demonstrated how custom modules can be used in schedules. However, for such a simple use case, there is a much shorter way of achieving the same goal. The following schedule will have the same effect, but without the use of any external Python module:
schedule: - temp: 22 if app.get_state("switch.take_a_bath") == "on" else Ignore() - temp: 19
Basically, we inlined the Python code we previously placed in
my_mod.py
right into the schedule rule. This works because it is
just an ordinary expression and not a series of statements. If you know
a little Python, you'll probably be familiar with this way of writing
expressions. Often, it is easier and also more readable to include such
short ones directly into the rule instead of calling external code.
This is a rule I use in my own Heaty configuration at home:
schedule_prepend: - temp: Add(-3) if app.get_state("input_boolean.absent") == "on" else Ignore()
What does this? Well, the first thing we see is that the rule is placed
inside the schedule_prepend
section. That means, it is valid for
every room and always the first rule being evaluated.
I've defined an input_boolean
called absent
in Home Assistant.
Whenever I leave the house, this gets enabled. If I return, it's turned
off again. In order for Heaty to notice the toggling, I added an
automation to Home Assistant which fires a heaty_reschedule
event.
How that can be done has already been shown above.
Now let's get back to the schedule rule. If it evaluates, it checks the
state of input_boolean.absent
. If the switch is turned on, it
evaluates to Add(-3)
, otherwise to Ignore()
.
Add(-3)
is no final temperature yet. Think of it as a temporary
value that is remembered and used later.
Now, my regular schedule starts being evaluated, which, of course, is
different for every room. Rules are evaluated just as normal. If one
returns a Result
, that is used as the temperature and evaluation
stops. But wait, there was the Add(-3)
, wasn't it? Sure it was.
Hence -3
is now added to the final result.
With this minimal configuration effort, I added an useful away-mode which throttles all thermostats in the house as soon as I leave.
Think of a device tracker that is able to report the distance between you and your home. Having such one set up, you could even implement dynamic throttling that slowly decreases as you near with almost zero configuration.
The Break
return type is most useful for disabling Heaty's
scheduling mechanism depending on the state of entities. You might
implement a schedule on/off switch with it, like so:
schedule_prepend: - temp: Break() if app.get_state("input_boolean.heating_schedule") == "off" else Ignore()
It has to be noted that temperature expressions are evaluated using
Python's eval()
function. In general, this is not suited for code
originating from a source you don't trust completely, because such code
can potentially execute arbitrary commands on your system with the same
permissions and capabilities the AppDaemon process itself has.
That shouldn't be a problem for temperature expressions you write
yourself inside schedules.
This feature could however become problematic if an attacker somehow
is able to emit events on your Home Assistant's event bus. To prevent
temperature expressions from being accepted in the heaty_set_temp
event, processing of such expressions is disabled by default and has
to be enabled explicitly by setting untrusted_temp_expressions: true
in your Heaty configuration.
Heaty introduces two new events it listens to:
heaty_reschedule
: Trigger a re-scheduling of the temperature. Parameters are:room_name
: the name of the room to re-schedule as defined in Heaty's configuration (not thefriendly_name
) (optional, default:null
, which means all rooms)
heaty_set_temp
: Sets a given temperature in a room. Parameters are:room_name
: the name of the room as defined in Heaty's configuration (not thefriendly_name
)temp
: a temperature expressionforce_resend
: whether to re-send the temperature to the thermostats even if it hasn't changed due to Heaty's records (optional, default:false
)reschedule_delay
: a number of minutes after which Heaty should automatically switch back to the schedule (optional, default: thereschedule_delay
set in Heaty's configuration for the particular room)
You can emit these events from your custom Home Assistant automations or scripts in order to control Heaty's behaviour.
This is an example Home Assistant script that turns the heating in the
room named living
to 25.0
degrees and switches back to the
regular schedule after one hour:
- alias: Hot for one hour sequence: - event: heaty_set_temp event_data: room_name: living temp: 25.0 reschedule_delay: 60
Schedules are not mandatory when using Heaty. It is perfectly valid to use Heaty just for controlling temperatures in rooms manually while still benefitting from other features like the open window detection.
To do so, just leave out everything that is related to schedules in
your apps.yaml
.