Using Home Assistant Packages as Project Containers

This is a structure I’ve arrived at that makes large YAML configurations much easier to maintain. I’m putting it out there because I’d like the pattern to be discussed and, if people find it useful, perhaps adopted more widely.

As a Home Assistant configuration grows, simply splitting files by domain eventually stops being enough.

You can have:

automations/
scripts/
input_booleans/
sensors/
...

but then everything belonging to different systems gets mixed together. A Roborock automation sits next to an alarm automation, which sits next to a lighting automation.

Home Assistant packages provide a way to group related configuration, but the common examples I found tend to use packages as individual YAML files containing several domain keys.

I wanted something slightly different.

I wanted a package to represent a project, while still keeping the configuration inside that project organized by Home Assistant domain.

The result is a kind of nested include structure:

configuration.yaml
  homeassistant:
    packages: !include _packages/packages.yaml
        ↓
_packages/packages.yaml
  roborock: !include roborock/roborock.yaml
        ↓
_packages/roborock/roborock.yaml
  input_boolean: !include_dir_merge_named input_booleans
  input_select: !include_dir_merge_named input_selects
  script: !include_dir_merge_named scripts
  automation: !include_dir_merge_list automations
        ↓
_packages/roborock/input_booleans/*.yaml
_packages/roborock/input_selects/*.yaml
_packages/roborock/scripts/*.yaml
_packages/roborock/automations/*.yaml

The important part is that every level has its own job.

packages.yaml knows about projects.

roborock.yaml knows which Home Assistant domains the Roborock project uses.

The individual files contain the actual configuration.

The package is a project container

My packages.yaml contains references to projects rather than configuration itself:

roborock: !include roborock/roborock.yaml

The project then defines its own domains:

input_boolean: !include_dir_merge_named input_booleans
input_select: !include_dir_merge_named input_selects
script: !include_dir_merge_named scripts
automation: !include_dir_merge_list automations

This gives me:

_packages/
└── roborock/
    ├── roborock.yaml
    ├── automations/
    ├── input_booleans/
    ├── input_selects/
    └── scripts/

So instead of having one huge roborock.yaml, the project itself has the same domain-based organization that I would normally use globally.

The individual files don’t contain package keys

This is an important part of the design.

For example:

# _packages/roborock/input_booleans/auto_clean.yaml

auto_clean:
  name: Auto clean

There is no:

input_boolean:

in this file.

That key is supplied by the parent include:

input_boolean: !include_dir_merge_named input_booleans

Likewise, a script can simply be:

clean_kitchen:
  sequence:
    ...

because the project file already establishes the script: context.

For understanding the structure, I think of each include as textual substitution. The include itself disappears and the resulting content takes its place, while the YAML context around it remains.

The final configuration is therefore conceptually no different from having written everything out manually.

Every include has its own relative path

Another important detail is that include paths are relative to the file containing the include.

So this:

# _packages/roborock/roborock.yaml

script: !include_dir_merge_named scripts

refers to:

_packages/roborock/scripts/

It does not refer to some global scripts/ directory.

This is what makes the nested structure possible.

Each level effectively establishes a new reference point:

configuration.yaml
        ↓
_packages/packages.yaml
        ↓
_packages/roborock/roborock.yaml
        ↓
_packages/roborock/scripts/

_shared still exists

I don’t use packages for everything.

My general configuration still lives in _shared:

_shared/
├── automations/
├── binary_sensors/
├── command_line/
├── input_booleans/
├── input_numbers/
├── input_selects/
├── scripts/
├── sensors/
└── ...

For example, my main configuration can contain:

automation: !include_dir_merge_list _shared/automations/
input_boolean: !include_dir_merge_named _shared/input_booleans/
script: !include_dir_merge_named _shared/scripts/
sensor: !include_dir_merge_list _shared/sensors/

So I have two different levels of organization:

_shared/
    general and small things

_packages/
    self-contained projects

A tiny automation doesn’t need a project directory.

If something grows into a coherent system, it can become a project.

Moving things between the two is easy

Because the individual files don’t contain package keys, moving configuration between _packages and _shared doesn’t require rewriting the file.

For example:

_packages/roborock/input_booleans/auto_clean.yaml

can later become:

_shared/input_booleans/auto_clean.yaml

without changing the contents.

The context changes because the file is included somewhere else, but the configuration itself doesn’t need to change.

This also means that using packages doesn’t lock configuration into packages.

I use a project template

To make creating new projects consistent, I keep a template project directory.

It contains a template YAML file with all the domain include options commented out:

# --- Entity-ID-keyed domains ---

# group: !include_dir_merge_named groups
# input_boolean: !include_dir_merge_named input_booleans
# input_select: !include_dir_merge_named input_selects
# input_number: !include_dir_merge_named input_numbers
# input_text: !include_dir_merge_named input_text
# input_datetime: !include_dir_merge_named input_datetime
# script: !include_dir_merge_named scripts
# rest_command: !include_dir_merge_named rest_commands
# utility_meter: !include_dir_merge_named utility_meters
# counter: !include_dir_merge_named counters
# shell_command: !include_dir_merge_named shell_commands

# --- List-based / platform domains ---

# automation: !include_dir_merge_list automations
# scene: !include_dir_merge_list scenes
# template: !include_dir_merge_list templates
# rest: !include_dir_merge_list rest_sensors
# switch: !include_dir_merge_list switches
# sensor: !include_dir_merge_list sensors
# binary_sensor: !include_dir_merge_list binary_sensors
# light: !include_dir_merge_list lights
# notify: !include_dir_merge_list notify
# command_line: !include_dir_merge_list command_line

But the template isn’t just the YAML file.

The domain directories are already there too.

So when I create a new project, I can simply copy the entire template directory:

_template/
├── _template.yaml
├── automations/
├── binary_sensors/
├── command_line/
├── counters/
├── groups/
├── input_booleans/
├── input_datetime/
├── input_numbers/
├── input_selects/
├── input_text/
├── lights/
├── notify/
├── rest_commands/
├── rest_sensors/
├── scenes/
├── scripts/
├── sensors/
├── shell_commands/
├── switches/
├── templates/
└── utility_meters/

Then I rename the YAML file to the project name and remove the # from the domains I actually need.

For example, if a project needs only automations, scripts and input booleans:

input_boolean: !include_dir_merge_named input_booleans
script: !include_dir_merge_named scripts
automation: !include_dir_merge_list automations

The corresponding directories already exist because they came from the template.

Unused directories can simply remain empty.

The template also prevents dangling includes

I use a simple rule:

A domain include and its directory are always enabled together.

So if this is commented:

# sensor: !include_dir_merge_list sensors

the sensors directory doesn’t need to exist.

If I uncomment it, the sensors directory is created or copied from the template.

This avoids having project files containing include statements pointing at directories that don’t exist.

The template therefore acts as a small scaffold for new projects rather than merely being a list of examples.

Adding a new project

Once the initial structure exists, adding a project is very simple.

I copy the template:

_template/

to:

roborock/

Then I configure the domains I actually use and add one line to packages.yaml:

roborock: !include roborock/roborock.yaml

That’s it.

The main configuration file doesn’t need to change.

Adding another project follows exactly the same pattern:

_packages/
├── packages.yaml
├── roborock/
├── alarm/
├── lighting/
└── something_else/

Each project is internally self-contained.

It is also non-destructive

One reason I like this approach is that it doesn’t require replacing my existing configuration.

The old _shared domain includes remain exactly where they were.

If I ever decide that I don’t want to use packages anymore, I can remove:

homeassistant:
  packages: !include _packages/packages.yaml

from the main configuration.

The _shared configuration continues to work because it was never dependent on the package structure.

This makes experimenting with the organization relatively low-risk.

The resulting mental model

The structure ends up being:

project
    ↓
Home Assistant domain
    ↓
individual configuration

rather than putting everything into global domain directories:

Home Assistant domain
    ↓
configuration from many unrelated projects

For me, that makes a large configuration much easier to navigate.

When I want to work on the Roborock system, I open roborock/.

When I want to work on a general automation, I look in _shared/automations/.

I don’t have to search through one enormous collection of unrelated automations, scripts and helpers.

The nice thing is that none of this requires a special mechanism beyond Home Assistant’s existing !include and !include_dir_merge_* functionality.

The useful part is the combination:

packages provide the project boundary, nested includes provide the domain boundary, and the individual YAML files contain only the actual configuration.

That particular combination is what I was looking for, and I couldn’t find much documentation describing it this way.

Once the structure is built, however, adding and maintaining projects becomes remarkably simple.

That still seems WAY overcomplicated to me. And that seems to be reflected in how long the post was needed to be to explain it.

You still end up with a whole bunch of different yaml files that you have to open and close to find the different pieces of the project. And you end up with a file tree from hell in your config directory.

By separating projects into a single package yaml file with all of the relevant domains/pieces included there is just one file per project to open.

I personally would find it way too much to have to keep going back to the file tree to open different yaml files to, for example, add an input_boolean, then an input_datetime, then a script, then an automation.

If I open one package file and I maintain a consistent domain structure within all package files scrolling to the domain I need to work on is trivial.

But everyone has what they think to them makes sense.

Exactly what I thought. I use functional packages, grouping e.g. all security-related work, or all weather-related work. I use the same principal as in programming: if one package gets too large, I try to break it up. I don’t want to jump between files when working on a concept or issue. That’s my preference, at least.

That is exactly where our configurations differ in scale. My weather project currently contains 104 files across 11 domains: 43 templates, 17 automations, 16 input numbers, sensors, REST resources, helpers, and other supporting configuration. Putting that into one functional weer.yaml would technically keep the concept together, but it would create a very large mixed-domain file. I use the package as the project boundary and the directories as the internal domain boundaries. For a small weather configuration, I would also use one functional package. In my case, the project has become large enough to need another level of organisation.

Example:

weer/
├── weer.yaml
├── automation/
│   ├── klimaat.yaml
│   ├── weer_anchor_spoor_dauwpunt.yaml
│   ├── weer_anchor_spoor_luchtdruk.yaml
│   ├── weer_anchor_spoor_temperatuur.yaml
│   ├── weer_bereken_atmosferische_onrust_batterij.yaml
│   ├── weer_bereken_atmosferische_onrust_keerzijde.yaml
│   ├── weer_eigen_dagreeks_temperatuur.yaml
│   ├── weer_familie_duur_automation.yaml
│   ├── weer_luchtdruk.yaml
│   ├── weer_persistentie_update_dauwpunt.yaml
│   ├── weer_persistentie_update_luchtdruk.yaml
│   ├── weer_persistentie_update_temperatuur.yaml
│   ├── weer_regime_bezetting_Xdagen_update.yaml
│   ├── weer_regime_duur_automation.yaml
│   ├── weer_rest_reload_dagindex_normalen_updaten.yaml
│   ├── weer_rest_reload_dagindex_normalen_updaten_1961-1990.yaml
│   ├── weer_rest_reload_dagindex_normalen_updaten_2001-2025.yaml
│   └── weer_snapshots_input_helper_4uur_16uur_temperatuur.yaml
├── input_boolean/
│   ├── weer_era5_vergelijk_tonen.yaml
│   ├── weer_gauge_barometer_zoom.yaml
│   ├── weer_gauge_weergave_relatief.yaml
│   ├── weer_klimaat_alert_status.yaml
│   ├── weer_klimaat_zichtbaar.yaml
│   └── weer_luchtdruk_dagelijkse_melding.yaml
├── input_datetime/
│   ├── weer_regime_bezetting_Xdagen_venster.yaml
│   ├── weer_start_familie.yaml
│   └── weer_start_regime.yaml
├── input_number/
│   ├── weer_atmosferische_onrust.yaml
│   ├── weer_atmosferische_onrust_batterij.yaml
│   ├── weer_atmosferische_onrust_keerzijde.yaml
│   ├── weer_fase_pad_dagen_terug.yaml
│   ├── weer_persistentie_dauwpunt_uren.yaml
│   ├── weer_persistentie_luchtdruk_uren.yaml
│   ├── weer_persistentie_temperatuur_uren.yaml
│   ├── weer_regime_bezetting_Xdagen_helpers.yaml
│   ├── weer_snapshot_4uur_16uur_temperatuur.yaml
│   ├── weer_snapshot_luchtdruk_1u.yaml
│   ├── weer_snapshot_luchtdruk_2u.yaml
│   ├── weer_snapshot_luchtdruk_3u.yaml
│   ├── weer_spoor_dauwpunt.yaml
│   ├── weer_spoor_luchtdruk.yaml
│   └── weer_spoor_temperatuur.yaml
├── input_select/
│   ├── weer_dashboard_tabs.yaml
│   └── weer_regime_bezetting_Xdagen_venster.yaml
├── input_text/
│   └── weer_input_text.yaml
├── rest/
│   ├── era5_jaarreeks_temperatuur.yaml
│   ├── era5_vergelijkingsperiode_1961-1990.yaml
│   ├── era5_vergelijkingsperiode_1991-2020_standaard.yaml
│   ├── era5_vergelijkingsperiode_2001-2025.yaml
│   └── weer_eigen_dagreeks_temperatuur_rest.yaml
├── script/
│   └── eigen_dagreeks_temperatuur_bijwerken.py
├── sensor/
│   ├── sensors_weer_dauwpunt.yaml
│   ├── sensors_weer_dauwpunt_minmax.yaml
│   ├── sensors_weer_luchtdruk.yaml
│   ├── sensors_weer_luchtdruk_minmax.yaml
│   ├── sensors_weer_luchtdruk_statistics.yaml
│   ├── sensors_weer_temperatuur.yaml
│   ├── sensors_weer_temperatuur_minmax.yaml
│   ├── sensors_weer_ventilatie.yaml
│   └── temp_buiten_gemiddeld_24u.yaml
├── shell_command/
│   └── weer_shell_commands.yaml
└── template/
    ├── klimaat_alert.yaml
    ├── weer_anomalie_dauwpunt.yaml
    ├── weer_anomalie_luchtdruk.yaml
    ├── weer_anomalie_temperatuur.yaml
    ├── weer_balkon_comfort.yaml
    ├── weer_barometrische_getijde_anomalie.yaml
    ├── weer_barometrische_glijdende_anomalie.yaml
    ├── weer_biometeo.yaml
    ├── weer_dagpositie_dauwpunt.yaml
    ├── weer_dagpositie_luchtdruk.yaml
    ├── weer_dagpositie_temperatuur.yaml
    ├── weer_dauwpunt.yaml
    ├── weer_familie_duur.yaml
    ├── weer_fase_pad_geschiedenis.yaml
    ├── weer_fase_pad_snapshots.yaml
    ├── weer_fase_pad_temp_voor_3D_plot.yaml
    ├── weer_fysica.yaml
    ├── weer_geïntegreerde_atmosferische_onrust.yaml
    ├── weer_index_atmosferische_alarmindex.yaml
    ├── weer_index_atmosferische_dynamiek_index.yaml
    ├── weer_index_convectieve_onrust_index.yaml
    ├── weer_knmi.yaml
    ├── weer_luchtdruk.yaml
    ├── weer_regime_bezetting_Xdagen.yaml
    ├── weer_regime_bezetting_Xdagen_log.yaml
    ├── weer_regime_transitie_projectie.yaml
    ├── weer_regime_wisseling.yaml
    ├── weer_stuwing_dag-min-max.yaml
    ├── weer_synoptisch.yaml
    ├── weer_synoptische_status_analyse.yaml
    ├── weer_temperatuur_buiten.yaml
    ├── weer_thermodynamica.yaml
    ├── weer_ventilatie_advies.yaml
    └── weer_zambretti_voorspelling.yaml