Including multiple template sensor files in packages

I have a new Home Assistant install (on a raspberry pi, from the official Home Assistant image, version 2025.12.2).

Over time I’m planning to automate a lot of things, at the moment I’m aiming to automate an irrigation system. This is modelled on one I previously had in OpenHab (moving away from Openhab), and will be based around an ESPHome set of switches (which are still shipping to me, so I’m using dummy switches in the meantime).

I’ve been extensively using AI (Grok and Gemini) to help me configure this, which works reasonably well, until it doesn’t. Both of them are in a loop with the problem I currently have, and I think that’s because the configuration style in this area has changed quite a bit recently, and so they’re reading conflicting documents and giving bad advice.

What I’m trying to do is to start the way I want to continue - to break my configuration into packages organised around a section of functionality, and to use the newest syntax for each element, not use deprecated syntax.

That means using template sensors for my sensors. I’m having a lot of difficulty getting a syntax that will let me include a sensors yaml file from each of my packages - at the moment it’s only reading one, and ignoring the other. It’s possible that I just have a syntax or inclusion error, however both AIs claim it’s because I can’t use “include_dir_named” on my packages without getting conflicts.

My folder structure is:

   config
      configuration.yaml
      packages
        irrigation.yaml
        weather.yaml
        irrigation
           some other yaml files
           sensors.yaml
        weather
           some other yaml files
           sensors.yaml

The behaviour I have is that when I copy all my sensors into the config/sensors/weather/sensors.yaml file (i.e. all sensors in a single file), they correctly parse and work. When I split them between config/sensors/weather/sensors.yaml and config/sensors/irrigation/sensors.yaml (i.e. two separate includes both for sensors), only those in weather load.

I have tried with a few different syntaxes, in particular I’ve tried with the top level of the sensors.yaml being:

template:
  - sensors:
    - name: .....

Or

template:
  sensors:
    - name: ....

Both are behaving the same. Any assistance with pointing me in the right direction would be greatly appreciated. It feels like it’s just going to be a simple syntax thing, but I can’t for the life of me work out what it is. The AIs have variously suggested reverting to the legacy sensor syntax (not a good idea), using instead a different include (perhaps include_dir_list, but that would mean reindenting a lot of the yaml, and probably changing the packaging approach), or just putting them all in one file (which works, but feels like it’s giving up instead of doing things properly).

The actual files are as below.

# configuration.yaml

# Loads default set of integrations. Do not remove.
default_config:

# Load frontend themes from the themes folder
frontend:
  themes: !include_dir_merge_named themes

scene: !include scenes.yaml

# This tells HA to look for files in the packages folder.
# Each file (like irrigation.yaml) becomes a package.
homeassistant: 
  packages: !include_dir_named packages
# irrigation.yaml
# ----------------------------------------------------------------------
# PACKAGE: Irrigation
# INTENT: Manifest file loading all irrigation components
# ----------------------------------------------------------------------
  
homeassistant:
  packages: !include_dir_named irrigation
  
# weather.yaml
# ----------------------------------------------------------------------
# PACKAGE: Weather
# INTENT: Manifest file loading all weather components
# ----------------------------------------------------------------------

homeassistant:
  packages: !include_dir_named weather
# ----------------------------------------------------------------------
# PACKAGE: Irrigation
# FILE:    sensors.yaml
# INTENT:  The watering adjustors based on seasons and recent rainfall,
#          and configuration for how often we apply fertiliser to each line type
#          Annoyingly the internal name that home assistant uses is based on the name, not
#          the unique id provided. So you can't choose a human friendly name for the sensor
#          whilst also having a coding-friendly id. Crazy, but there it is. You would think
#          the unique_id field would do that, but apparently it doesn't. I think I can torture
#          it to do what I want though by first creating a name field that will slugify to the
#          unique id I want, then change that name field after the sensor has been instantiated.
#
#          This file is currently refusing to load, Gemini claims it's an include
#          problem. Whilst working through that, all these sensors are copied into the
#          weather sensors file.
# ----------------------------------------------------------------------

template:
  - sensor:
    - name: "Seasonal Watering Multiplier"     
      unique_id: seasonal_watering_multiplier
      unit_of_measurement: "%"                          
      state: >-                                     
        {{ [150,180,180,130,60,35,20,20,35,60,90,120][now().month-1] }}
      #     J   F   M   A   M  J  J  A  S  O  N  D                      
                                                    
    - name: "Landscape Water Every"
      unique_id: landscape_water_every
      state: >-
        {{ [5,5,7,10,15,0,0,0,15,10,7,5][now().month-1] }}   
      #     J F M A  M  J J A S  O  N D  

    - name: "Fruit Water Every"
      unique_id: fruit_water_every
      state: >-
        {{ [2,2,2,3,4,6,0,0,0,5,3,2][now().month-1] }}   
      #     J F M A M J J A S O N D  

    - name: "Landscape Fertilise Every"
      unique_id: landscape_fertilise_every
      state: >-
        {{ [4,4,4,4,4,4,0,0,0,0,0,4][now().month-1] }}  
      #     J F M A M J J A S O N D  

    - name: "Fruit Fertilise Every"
      unique_id: fruit_fertilise_every
      state: >-
        {{ [2,2,2,2,2,3,0,0,0,0,0,2][now().month-1] }}  
      #     J F M A M J J A S O N D  

    # Day of Year Helper
    - name: "Day of Year"
      unique_id: day_of_year
      state: "{{ now().timetuple().tm_yday }}"

    # ----------------------------------------------------------------------
    # RAIN LONG ADJUST %  →  Used for trees, lawns (soil reservoir)
    # ----------------------------------------------------------------------
    - name: "Rain Long Adjust %" 
      unique_id: rain_long_adjust
      unit_of_measurement: "%"
      state: >                   
        {% set m = now().month %}                        
        {% set ratchet = [3,3,3,3,2,2,1,1,1,2,3,3][m-1] %}        
        {% set r2  = states('sensor.rain_last_2_days')|float(0) %}
        {% set r5  = states('sensor.rain_last_5_days')|float(0) %} 
        {% set r15 = states('sensor.rain_last_15_days')|float(0) %}
        {% set r30 = states('sensor.rain_last_30_days')|float(0) %}
                                                    
        {% if r2 > 3 * ratchet %}             0
        {% elif r5 < 1 * ratchet %}             
          {% if r30 < 3 * ratchet %}         180
          {% elif r15 < 3 * ratchet %}       150
          {% elif r15 < 5 * ratchet %}       120
          {% elif r15 < 7 * ratchet %}       110
          {% else %}                         100
          {% endif %}                           
        {% elif r5 < 2 * ratchet %}          100
        {% elif r5 < 4 * ratchet %}           80
        {% elif r5 < 6 * ratchet %}           60
        {% else %}                            0
        {% endif %}
        
    # ----------------------------------------------------------------------
    # RAIN SHORT ADJUST % →  Used for raised beds, vege garden
    # ----------------------------------------------------------------------
    - name: "Rain Short Adjust %" 
      unique_id: rain_short_adjust
      unit_of_measurement: "%"
      state: >                   
        {% set m = now().month %}                        
        {% set ratchet = [3,3,3,3,2,2,1,1,1,2,3,3][m-1] %}       
        {% set r2 = states('sensor.rain_last_2_days')|float(0) %}
        {% if r2 > 7 * ratchet %}        0
        {% elif r2 < 2 * ratchet %}    100
        {% elif r2 < 5 * ratchet %}     35
        {% else %}                      65
        {% endif %}

    # ----------------------------------------------------------------------
    # DYNAMIC GROUP: ALL IRRIGATION VALVES (For Emergency Stop)
    # Intent: Automatically finds all entities that start with 'switch.zone_'
    # ----------------------------------------------------------------------
    - name: "All Irrigation Valves List"
      unique_id: all_irrigation_valves_list
      state: "{{ now().timestamp() }}" # Forces regular update
      attributes:
        entity_ids: >
          {% set zone_switches = states.switch 
                                | selectattr('entity_id', 'match', 'switch.zone_.*') 
                                | map(attribute='entity_id') | list %}
          {{ zone_switches + ['switch.fertiliser_injector'] | sort }} # Add injector explicitly
          
    # ----------------------------------------------------------------------
    # DYNAMIC GROUP: LANDSCAPE TREES VALVES
    # Intent: Finds all entities that start with 'switch.zone_landscape_'
    # ----------------------------------------------------------------------
    - name: "Landscape Trees Valves List"
      unique_id: landscape_valves_list
      state: "{{ now().timestamp() }}"
      attributes:
        entity_ids: >
          {% set pattern = 'switch.zone_landscape_' %}
          {{ states.switch 
            | selectattr('entity_id', 'match', pattern ~ '.*') 
            | map(attribute='entity_id') | list | sort }}

    # ----------------------------------------------------------------------
    # DYNAMIC GROUP: FRUIT TREES VALVES
    # Intent: Finds all entities that start with 'switch.zone_fruit_'
    # ----------------------------------------------------------------------
    - name: "Fruit Trees Valves List"
      unique_id: fruit_valves_list
      state: "{{ now().timestamp() }}"
      attributes:
        entity_ids: >
          {% set pattern = 'switch.zone_fruit_' %}
          {{ states.switch 
            | selectattr('entity_id', 'match', pattern ~ '.*') 
            | map(attribute='entity_id') | list | sort }}
# ----------------------------------------------------------------------
# PACKAGE: Weather
# FILE:    sensors.yaml
# INTENT:  Sensors that accumulate rainfall over time - last day, last 2 days, last 5 days
#          Annoyingly the internal name that home assistant uses is based on the name, not
#          the unique id provided. So you can't choose a human friendly name for the sensor
#          whilst also having a coding-friendly id. Crazy, but there it is. You would think
#          the unique_id field would do that, but apparently it doesn't. I think I can torture
#          it to do what I want though by first creating a name field that will slugify to the
#          unique id I want, then change that name field after the sensor has been instantiated.
#
#          The irrigation sensors file is currently refusing to load, Gemini claims it's an include
#          problem. Whilst working through that, all those sensors are copied into this
#          weather sensors file.
# ----------------------------------------------------------------------

# -------  Rain accumulation sensors ---------
# Robustness: float(0) ensures no errors if history is missing
template:
  - sensor:
    - name: "Rain Last 2 Days"
      unique_id: rain_last_2_days
      unit_of_measurement: "mm"
      device_class: precipitation
      state: >-
        {{ [states('sensor.rain_accumulation_daily')|float(0),
            states('sensor.rain_accumulation')|float(0)] | sum | round(1) }}

    - name: "Rain Last 3 Days"
      unique_id: rain_last_3_days
      unit_of_measurement: "mm"
      state: >-
        {{ [states('sensor.rain_accumulation_daily')|float(0),
            states('sensor.rain_accumulation_2_days_ago')|float(0),
            states('sensor.rain_accumulation')|float(0)] | sum | round(1) }}

    - name: "Rain Last 5 Days"
      unique_id: rain_last_5_days
      unit_of_measurement: "mm"
      state: >-
        {{ [states('sensor.rain_accumulation_daily')|float(0),
            states('sensor.rain_accumulation_2_days_ago')|float(0),
            states('sensor.rain_accumulation_3_days_ago')|float(0),
            states('sensor.rain_accumulation_4_days_ago')|float(0),
            states('sensor.rain_accumulation')|float(0)] | sum | round(1) }}

    - name: "Rain Last 15 Days"
      unique_id: rain_last_15_days
      unit_of_measurement: "mm"
      state: >-
        {% set s = 'sensor.rain_accumulation' %}
        {% set days = 14 %}
        {% set total = states(s)|float(0) %}
        {% for i in range(days) %}
          {% set total = total + states(s ~ '_' ~ i ~ '_days_ago')|float(0) %}
        {% endfor %}
        {{ total | round(1) }}

    - name: "Rain Last 30 Days"
      unique_id: rain_last_30_days
      unit_of_measurement: "mm"
      state: >-
        {% set s = 'sensor.rain_accumulation' %}
        {% set days = 29 %}
        {% set total = states(s)|float(0) %}
        {% for i in range(days) %}
          {% set total = total + states(s ~ '_' ~ i ~ '_days_ago')|float(0) %}
        {% endfor %}
        {{ total | round(1) }}

    - name: "Seasonal Watering Multiplier"     
      unique_id: seasonal_watering_multiplier
      unit_of_measurement: "%"                          
      state: >-                                     
        {{ [150,180,180,130,60,35,20,20,35,60,90,120][now().month-1] }}
      #     J   F   M   A   M  J  J  A  S  O  N  D                      
                                                    
    - name: "Landscape Trees Water Every Days"
      unique_id: landscape_water_every
      state: >-
        {{ [5,5,7,10,15,0,0,0,15,10,7,5][now().month-1] }}   
      #     J F M A  M  J J A S  O  N D  

    - name: "Fruit Trees Water Every Days"
      unique_id: fruit_water_every
      state: >-
        {{ [2,2,2,3,4,6,0,0,0,5,3,2][now().month-1] }}   
      #     J F M A M J J A S O N D  

    - name: "Landscape Trees Fertilise Every Days"
      unique_id: landscape_fertilise_every
      state: >-
        {{ [4,4,4,4,4,4,0,0,0,0,0,4][now().month-1] }}  
      #     J F M A M J J A S O N D  

    - name: "Fruit Trees Fertilise Every Days"
      unique_id: fruit_fertilise_every
      state: >-
        {{ [2,2,2,2,2,3,0,0,0,0,0,2][now().month-1] }}  
      #     J F M A M J J A S O N D  

    # Day of Year Helper
    - name: "Day of Year"
      unique_id: day_of_year
      state: "{{ now().timetuple().tm_yday }}"

    # ----------------------------------------------------------------------
    # RAIN LONG ADJUST %  →  Used for trees, lawns (soil reservoir)
    # ----------------------------------------------------------------------
    - name: "Rain Long Adjust %" 
      unique_id: rain_long_adjust
      unit_of_measurement: "%"
      state: >                   
        {% set m = now().month %}                        
        {% set ratchet = [3,3,3,3,2,2,1,1,1,2,3,3][m-1] %}        
        {% set r2  = states('sensor.rain_last_2_days')|float(0) %}
        {% set r5  = states('sensor.rain_last_5_days')|float(0) %} 
        {% set r15 = states('sensor.rain_last_15_days')|float(0) %}
        {% set r30 = states('sensor.rain_last_30_days')|float(0) %}
                                                    
        {% if r2 > 3 * ratchet %}             0
        {% elif r5 < 1 * ratchet %}             
          {% if r30 < 3 * ratchet %}         180
          {% elif r15 < 3 * ratchet %}       150
          {% elif r15 < 5 * ratchet %}       120
          {% elif r15 < 7 * ratchet %}       110
          {% else %}                         100
          {% endif %}                           
        {% elif r5 < 2 * ratchet %}          100
        {% elif r5 < 4 * ratchet %}           80
        {% elif r5 < 6 * ratchet %}           60
        {% else %}                            0
        {% endif %}
        
    # ----------------------------------------------------------------------
    # RAIN SHORT ADJUST % →  Used for raised beds, vege garden
    # ----------------------------------------------------------------------
    - name: "Rain Short Adjust %" 
      unique_id: rain_short_adjust
      unit_of_measurement: "%"
      state: >                   
        {% set m = now().month %}                        
        {% set ratchet = [3,3,3,3,2,2,1,1,1,2,3,3][m-1] %}       
        {% set r2 = states('sensor.rain_last_2_days')|float(0) %}
        {% if r2 > 7 * ratchet %}        0
        {% elif r2 < 2 * ratchet %}    100
        {% elif r2 < 5 * ratchet %}     35
        {% else %}                      65
        {% endif %}

    # ----------------------------------------------------------------------
    # DYNAMIC GROUP: ALL IRRIGATION VALVES (For Emergency Stop)
    # Intent: Automatically finds all entities that start with 'switch.zone_'
    # ----------------------------------------------------------------------
    - name: "All Irrigation Valves List"
      unique_id: all_irrigation_valves_list
      state: "{{ now().timestamp() }}" # Forces regular update
      attributes:
        entity_ids: >
          {% set zone_switches = states.switch 
                                | selectattr('entity_id', 'match', 'switch.zone_.*') 
                                | map(attribute='entity_id') | list %}
          {{ zone_switches + ['switch.fertiliser_injector'] | sort }} # Add injector explicitly
          
    # ----------------------------------------------------------------------
    # DYNAMIC GROUP: LANDSCAPE TREES VALVES
    # Intent: Finds all entities that start with 'switch.zone_landscape_'
    # ----------------------------------------------------------------------
    - name: "Landscape Trees Valves List"
      unique_id: landscape_valves_list
      state: "{{ now().timestamp() }}"
      attributes:
        entity_ids: >
          {% set pattern = 'switch.zone_landscape_' %}
          {{ states.switch 
            | selectattr('entity_id', 'match', pattern ~ '.*') 
            | map(attribute='entity_id') | list | sort }}

    # ----------------------------------------------------------------------
    # DYNAMIC GROUP: FRUIT TREES VALVES
    # Intent: Finds all entities that start with 'switch.zone_fruit_'
    # ----------------------------------------------------------------------
    - name: "Fruit Trees Valves List"
      unique_id: fruit_valves_list
      state: "{{ now().timestamp() }}"
      attributes:
        entity_ids: >
          {% set pattern = 'switch.zone_fruit_' %}
          {{ states.switch 
            | selectattr('entity_id', 'match', pattern ~ '.*') 
            | map(attribute='entity_id') | list | sort }}

The issue is your folder structure and the fact that you have multiple files with the same name. HA use the filename to build a slug that is used to create a combined version of the YAML.

I would do this folder structure:

/config
  configuration.yaml
  /packages
    /irrigation
      irr_sensors.yaml
      irr_config.yaml
    /weather
      weather_sensors.yaml
      weather_xxx.yaml
    some_small_package.yaml

I did not read through your whole post. I like to help people, but I make it a point not to debug AI generated code because it takes a lot of effort to understand things that in the end were illogical in the first place.

AI is not well suited for the task if you do not understand what it does. Your problems are with understanding the structure of yaml and the ways in which you can split up yaml to organize it. Each method has different requirements to the content of the files. If you do not understand those, you cannot see where AI goes wrong, and you cannot move stuff around.

So TL;DR: AI does not free you from needing to know stuff. You are finding it out the hard way. AI gives you a lot fast, but getting the kinks out takes ages, even if you do understand what AI did.

Yaml itself is very straightforward, there are only a few concept on how to structure data. PS: That has nothing to do with how HA represents things like sensors and automations in yaml. Yaml is just a data structure representation. So read up on those concepts, a general tutoral on YAML, not HA specific. Why the is indentation important, what does a - before it mean, that sort of stuff.

Then read the following two pieces of documentation on ways to split up the configuration. HA docs are always op to date:

1 Like

sensors: should be sensor: in both your examples.

The right syntax is

template:
  - sensor:
     - name: foo
       state: "{{ template }}"

It doesn’t really matter if you add multiple sensor items under template or just multiple items under sensor.
It does matter when you want to use a trigger based template sensor though.

I’ve read more of your post now :slight_smile:
You seem to be defining packages twice, once in your configuration.yaml and then again for each package. Don’t do that, it doesn’t work, and there is no need for it.

Good spot. Although if you use the folder structure I posted the irrigation.yaml and weather.yaml files are not needed.

The key here is that HA uses the filename as a slug and hence filenames under the packages folder should be unique. Maybe worth making a feature request to build the slug with folder names as they are currently ignored.

Thank you, this is the answer. I read a lot of forum posts, and all the documentation, but that is the first place I’ve found that particular nugget of information, that filenames need to be globally unique (even when within folders).

I had the feeling that it would be something obscure but obvious to someone who had more experience.

Sorry, those examples I typed from memory, the actual files correctly have “sensor:”.

Thanks for the tip on defining packages twice. It wasn’t clear to me from the documentation whether include_dir_name was recursive or not, for some reason I got the impression that it wasn’t and that I therefore had to include again to get sub directories. That was probably due to using AI - I think it suggested it and from my read of the documentation it seemed reasonable so I went with it.

I’ve submitted a pull request for documentation updates to clearly cover both that include_dir_named requires that file names are unique (even in different sub-folders), and that include_dir_named is recursive over sub folders. Both of these points were actually in the documentation, but I totally missed both of them despite reading that page 3 times. I have made them a bit clearer.