Post

Simplifying Home Assistant's configuration with ytt

How you can use templating for your Home Assistant configuration with ytt

Simplifying Home Assistant's configuration with ytt

I have been a light user of Home Assistant to automate my home for a few years, and have recently lost all my configuration (I could blame Raspberry Pi’s SD card usage for that, but I should have anticipated with backups…).
This was a good opportunity to reconfigure everything “as code” (using YAML files instead of the GUI), and in this post I will show how I have simplified my configuration using ytt.

What is ytt ?

Ytt stands for “YAML Templating Tool”, it adds templating syntax to YAML files and can be used in many “YAML-oriented” scenarios: K8S configuration, CI/CD pipelines authoring, etc.
It’s part of the Carvel set of tools, which is a CNCF Sandbox project.
The best way to start with ytt is the official documentation here, but in a nutshell it uses a specific comment syntax starting with #@ to turn any YAML file into a template:

1
2
3
4
#@ for room in ['Bedroom', 'Living room', 'Bathroom']:
- platform: generic_thermostat
  name: #@ "{} thermostat".format(room)
#@ end

This (uncompleted) snippet will for instance create 3 thermostats with distincts names: Bedroom thermostat, Living room thermostat and Bathroom thermostat.
Ytt has many features and can be used in many ways. If you are new to ytt, the best way to start learning is the interactive playground to gradually go through its key concepts.

My Home Assistant setup

I assume that if you are still reading this you are familiar with Home Assistant but if it’s not the case, you should start here or here.

The only automated things in my home are electric heaters with “pilot wires”, each room contains the following physical devices:

  • A heater with a pilot wire
  • A on/off module connected to the pilot wire
  • A temperature/humidity sensor

The sensor and the on/off module communicate with Home Assistant using the RFXCOM integration. I’m currently planning to move to another platform but that’s not important here.

In Home Assistant, I just need to regulate the temperature according to a different schedule for each room, so I have:

  • A generic thermostat with presets (eco, comfort, away, …) for each room
  • A schedule to define when each thermostat should use the comfort preset
  • Automations to actually change the thermostat’s presets according to the schedules

Your installation is probably different but that doesn’t matter, what’s interesting here is how we can use ytt to simplifying editing Home Assistant’s YAML configuration files.

The YAML snippets in this post are just samples based on my usage, not the real files I am using for my installation (those are stored in a private repo).

How ytt can help

The main problem in my setup is that each room has the same devices but a proper configuration: the temperature schedule is not the same in my kid’s room and in my home office. So some settings are shared, others are specific.
Let’s see how ytt will help to deal with this situation.

DRY: Writing required YAML only

The only YAML I need to edit to adjust my home automation settings is a file that lists all the rooms in my house:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
rooms:
- name: living_room
  display_name: Living room
  weekend_confort_start: "08:45:00"
  weekend_confort_end: "22:00:00"
- name: home_office
  display_name: Home office
  eco_temp: 16
  comfort_temp: 18.5
  weekday_confort_am_start: "07:45:00"
  weekday_confort_pm_end: "18:00:00"
- name: bathroom
  display_name: Bathroom
  comfort_temp: 20
  weekday_confort_pm_end: "07:30:00"
- name: bedroom
  display_name: Bedroom
  eco_temp: 15
  comfort_temp: 18

It uses the ytt concept of data values, ie variables that can be referenced in templates.

Defining a schema for the rooms

In the previous files, you’ll notice that some properties are not declared for each room. There is indeed a schema document that defines the properties, sets default values, and if properties are mandatory or not:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#@data/values-schema
---
rooms:
#@schema/validation min_len=1
- name: ""
  #@schema/validation min_len=1
  display_name: ""
  min_temp: 14
  max_temp: 21
  away_temp: 15
  comfort_temp: 19.0
  eco_temp: 17
  keep_alive: "00:00:30"
  weekday_confort_am_start: "06:30:00"
  #@schema/nullable
  weekday_confort_am_end: "09:00:00"
  #@schema/nullable
  weekday_confort_pm_start: "17:00:00"
  weekday_confort_pm_end: "22:00:00"
  #@schema/nullable
  weekend_confort_start: "08:45:00"
  #@schema/nullable
  weekend_confort_end: "22:00:00"

This schema uses just a few features of ytt’s schema validations: mark some properties as required using schema/validation min_len and others as nullable using schema/nullable. This page in the docs is a great reference for the other schema validation possibilities.

Using the data and schema to generate Home Assistant’s objects

This is where the fun begins, let’s put the schema and values in action, starting with the generation of thermostats:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#@ load("@ytt:data", "data")
---
#@ for room in data.values.rooms:
- platform: generic_thermostat
  unique_id: #@ "thermostat_" +  room.name
  name: #@ "Thermostat in " + room.display_name)
  heater: #@ "switch.heater_" + room.name
  target_sensor: #@ "sensor.temp_" + room.name
  min_temp: #@ room.min_temp 
  max_temp: #@ room.max_temp
  away_temp: #@ room.away_temp
  comfort_temp: #@ room.comfort_temp
  eco_temp: #@ room.eco_temp
  keep_alive: #@ room.keep_alive
#@ end

This files uses a for loop to iterate through the rooms and declare for each room an object following the YAML schema of the generic_thermostat integration of Home Assistant.

Moving on to the schedules, the fun continues with for loops for week days and weekends inside a loop over the rooms:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#@ load("@ytt:data", "data")
---
#@ for room in data.values.rooms:
#@yaml/text-templated-strings
'schedule_thermostat_(@= room.name @)':
  name: #@ "Schedule for thermostat in " + room.display_name
  #@ for day in ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']:
  (@= day @):
    #@ if room.weekday_confort_am_end != None:
    - from: #@ room.weekday_confort_am_start
      to: #@ room.weekday_confort_am_end
    - from: #@ room.weekday_confort_pm_start
      to: #@ room.weekday_confort_pm_end
    #@ else:
    - from: #@ room.weekday_confort_am_start
      to: #@ room.weekday_confort_pm_end
    #@ end
  #@ end
  #@ if room.weekend_confort_start != None:
  #@ for day in ['saturday', 'sunday']:
  (@= day @):
    - from: #@ room.weekend_confort_start
      to: #@ room.weekend_confort_end
  #@ end
  #@ end
#@ end

A few things to mention in the above file:

  • The #@yaml/text-templated-strings line followed by 'schedule_thermostat_(@= room.name @)': brings the use of ytt’s text templating feature. It allows templating for property names, which is not possible using the comment-based (#@) syntax. The feature is also used on lines 8 and 21 ((@= day @)) to use the days names as properties.
  • The #@ if <condition> != None: lines shows how nullable values are handled in templates. Note that nullable properties are different from properties with default values

Lastly this the template file for automations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#@ load("@ytt:data", "data")
---
#@ for room in data.values.rooms:
- id: #@ "heating_{}_on".format(room.name)
  alias: #@ "Heating on in room {}".format(room.display_name)
  description: #@ "Switch to comfort preset in room {}".format(room.display_name)
  triggers:
      - trigger: state
        entity_id:
          - #@ "schedule.schedule_thermostat_{}".format(room.name)
        to: "on"
  actions:
    - action: climate.set_preset_mode
      target:
        entity_id: #@ "climate.thermostat_{}".format(room.name)
      data:
        preset_mode: comfort
- id: #@ "heating_{}_off".format(room.name)
  alias: #@ "Heating off in room {}".format(room.display_name)
  description: #@ "Switch to eco preset in room {}".format(room.display_name)
  triggers:
      - trigger: state
        entity_id:
          - #@ "schedule.schedule_thermostat_{}".format(room.name)
        to: "off"
  actions:
    - action: climate.set_preset_mode
      target:
        entity_id: #@ "climate.thermostat_{}".format(room.name)
      data:
        preset_mode: eco
#@ end

Nothing new in this file, each room will have two automations: one for setting its thermostat to the comfort preset when an event starts in its schedule, and one for setting it to the eco preset once the event ends.

Deploying to Home Assistant

Let’s see how to move this YAML files to Home Assistant. First I use the following script to generate the actual YAML files using ytt:

1
2
3
4
#!/bin/sh

script_dir=$(dirname "$0")
ytt -f "$script_dir/config" --data-values-file "$script_dir/values/rooms.yaml" --output-files "$script_dir/out"

The generated YAML files are output in an out directory, there is one file for automations, one for thermostats, etc.
Deploying these files to Home Assistant is done with the GUI using the File Editor add-on.
First, theses lines must be added to the main configuration file to reference the new files:

1
2
3
automation: !include automations.yaml
climate: !include climate.yaml
schedule: !include schedules.yaml

Then, each file can be created and edited using File Editor in the GUI. Once the file are up-to-date in Home Assistant, I run a configuration check and if everything is ok, I reload the configuration to apply the changes.

As you have seen my workflow is not fully automated, as I still deploy configuration files “by hand”. I might automate this step in the future but I’m happy with it for now.
Also it’s tricky to automate everything, for instance I add physical devices with the GUI as it requires physical interaction (pushing buttons) on each device, so it can’t be done with a zero-touch approach.

Adding new features

There is a lot of YAML content in this blog post, but it only implements simple thermostats for now. This is a good starting point, once you are familiar with this approach, you can extend your installation with new features.
For instance I have added a “heating mode” input select with several modes to automatically lower the temperature in the whole house when the family is away for a few days, put it back to normal a few hours before we are back home, switch all heaters off in springtime, etc.

It relies on the use of a new automation triggered when the input select changes, and conditions on existing automations.
I won’t go further in the details, as this post is already long enough. I encourage you to build-up your skills with ytt, as you might use it for other tasks, and explore the great documentation of Home Assistant to understand how you can manage your configuration with templated YAML files instead of performing many manual actions in the GUI. At least, it works pretty well for me.

Wrapping-up

That’s it for the last post of the year, this is my approach to bring an “as code” flavour to my Home Assistant usage. I hope this make sense and is useful, as I have written this post quickly and shared only snippets of my “code” instead of the full repo that is private.
Don’t hesitate to reach out in the comment if you need, cheers 🤓

This post is licensed under CC BY 4.0 by the author.