Skip to content

Enhanced YAML Configuration with Variables, Includes, Packages, and Anchors#4818

Closed
jimtng wants to merge 1 commit into
openhab:mainfrom
jimtng:yaml-preprocessor-secretless
Closed

Enhanced YAML Configuration with Variables, Includes, Packages, and Anchors#4818
jimtng wants to merge 1 commit into
openhab:mainfrom
jimtng:yaml-preprocessor-secretless

Conversation

@jimtng

@jimtng jimtng commented May 17, 2025

Copy link
Copy Markdown
Contributor

YAML Preprocessor, Includes, Packages, and Variable Substitution

Resolves: #3808, #4872, #5240
Related documentation PR: openhab/openhab-docs#2614

This PR introduces a YAML preprocessing layer to openHAB Core, enabling a more expressive and modular configuration syntax while preserving full backward compatibility with existing YAML files.


Summary of Changes

1. Exclude *.inc.yaml, *.inc.yml, and generated debug output from model loading

These files and directories are not meant to be parsed by YamlModelRepositoryImpl and are now excluded from model discovery:

  • *.inc.yaml and *.inc.yml — reusable fragments intended for !include, not standalone models
  • CONF/<yaml|items|things|tags>/_generated/ — contains fully resolved debug output produced only when the preprocessor.generate_resolved_file option is enabled (default: false)

2. Add a YAML Preprocessor

A new preprocessing stage is added before the YAML model repository loads the configuration.
The preprocessor expands the YAML file into a fully resolved document, supporting:

  1. Variable definitions and substitutions / interpolation
  2. File inclusion (!include)
  3. Package files (multi‑section reusable templates)
  4. YAML anchors & aliases

This enables cleaner structure, reduced duplication, and reusable templates for Things, Items, and related metadata.

Processing Flow

Previous flow:

YAML File → processModelContent

New flow:

YAML File → YamlPreprocessor → processModelContent

The YamlPreprocessor produces a fully expanded YAML document that the existing model loader can consume without modification.


Documentation

Full documentation for these features is available in the corresponding docs PR:
openhab/openhab-docs#2614

Preview:
https://deploy-preview-2614--openhab-docs-preview.netlify.app/docs/configuration/yaml/

Individual pages (optional quick links):

@jimtng jimtng requested a review from a team as a code owner May 17, 2025 10:44
This was referenced May 17, 2025
@jimtng

jimtng commented May 18, 2025

Copy link
Copy Markdown
Contributor Author

@lolodomo FYI.

This shouldn't conflict with anything that you're working on, I think.

@jimtng jimtng force-pushed the yaml-preprocessor-secretless branch from 704d716 to b4f551c Compare May 18, 2025 23:50
@jimtng

jimtng commented May 18, 2025

Copy link
Copy Markdown
Contributor Author

rebased to main

@jimtng jimtng force-pushed the yaml-preprocessor-secretless branch from b4f551c to 60bb8b0 Compare May 19, 2025 00:02
@jimtng jimtng marked this pull request as draft May 19, 2025 01:17
@jimtng jimtng force-pushed the yaml-preprocessor-secretless branch from 60bb8b0 to d04a9a2 Compare May 19, 2025 13:52
@jimtng jimtng marked this pull request as ready for review May 19, 2025 13:53
@jimtng

jimtng commented May 21, 2025

Copy link
Copy Markdown
Contributor Author

Todo: merge lists in packages

@kaikreuzer

Copy link
Copy Markdown
Member

@lolodomo When you find time, I would appreciate a short review from your end.

@spacemanspiff2007

Copy link
Copy Markdown
Contributor

Great Work!
I'll take a deeper look at this this week but some questions / remarks so I have a better understanding:

  1. Do per file variables need to be defined as global variables or is it sufficient to define them in the include directive?
  2. Shouldn't __PATH__ be named __FOLDER__, __DIRECTORY__ or __PARENT__
  3. Single quoted strings won't be interpolated,
    What's your reasoning or idea behind this?
  4. What is returned for ${var:-'{}'} if var is undefined?

@jimtng

jimtng commented May 26, 2025

Copy link
Copy Markdown
Contributor Author
  1. Do per file variables need to be defined as global variables or is it sufficient to define them in the include directive?

No, they only need to be defined in the include directive.

  1. Shouldn't __PATH__ be named __FOLDER__, __DIRECTORY__ or __PARENT__

Out of the three options, I prefer __DIRECTORY__. Is PATH too ambiguous?

  1. Single quoted strings won't be interpolated,
    What's your reasoning or idea behind this?

So that you can have ${xxx} without being interpolated?

  1. What is returned for ${var:-'{}'} if var is undefined?

Good question. As it is now, it will return '{}' - however, it will trip up when you have ${var:-blah}${var:-blah} because the pattern for the "default" value is greedy.

To fix this, I need to make it ungreedy, but it would no longer work for ${var:-'{}'} case. I think this is an acceptable compromise without making the regex pattern more complex or implementing a separate parser. So the next change will have this requirement: The default value cannot contain } character.

@jimtng

jimtng commented May 26, 2025

Copy link
Copy Markdown
Contributor Author

@spacemanspiff2007 I added a commit that supports quoted defaults (in which you can use braces).

  • ${var:-'{}'} => {} when var is undefined

@spacemanspiff2007

Copy link
Copy Markdown
Contributor

@jimtng I really like what you did in this PR! This is going to be a great feature which will help a lot and make configuration a breeze.


Is PATH too ambiguous?

In python there is a pathlib which deals with paths to dirs, files, drives, shares so PATH became synonymous to me with location of an arbitrary file system object. I think __DIRECTORY__ would be a nicer and also more correct.


Instead of packages how about we name the top level key templates or includes?
Package sounds like a couple of files yet it's only one file that's used.

Would it also be possible to not have to use the !include directive?
It's clear from the top level key that it's a reference to another file and what's actually happening is a merge and not an include.
E.g. like this:

packages:
  livingroom-light1:
    file: esphome-light.inc.yaml
    vars:
      thingid: livingroom-light1

How about we also add a templates? (or whatever name we settle for the top level key) and includes? subfolder to the YamlModelRepositoryImpl exclusion regex?


So that you can have ${xxx} without being interpolated?

I understand. I guess it's quite unexpected that " and ' quoted strings behave differently.
Maybe we can add the possibility to escape variable expansion with \${...} later or add a global option to not do the expansion for a file at all later. Then the behavior could be aligned which would be more user friendly.

@jimtng

jimtng commented May 27, 2025

Copy link
Copy Markdown
Contributor Author

@spacemanspiff2007

__PATH__ is now __DIRECTORY__ although I was tempted to shorten it to __DIR__. It was already done in a commit prior to your last message.

Instead of packages how about we name the top level key templates or includes?
Package sounds like a couple of files yet it's only one file that's used.

templates is definitely not correct for it.
includes is awkward, IMO.

I think packages is a good name. The idea originally came from https://esphome.io/components/packages.html

Package sounds like a couple of files yet it's only one file that's used.

The packages section would contain a list of includes, so it does contain several files/packages

packages:
  package1:
  package2:
  package3:

the package keys not being used bothers me a bit. Perhaps it should just be a list instead of a map, but I went with a map to make it all consistent with the rest of the convention for things, items, tags.

Perhaps I can pass an implicit variable ${PACKAGE_KEY} into the included file to make it useful.

Would it also be possible to not have to use the !include directive?

I'll consider it.

How about we also add a templates? (or whatever name we settle for the top level key) and includes? subfolder to the YamlModelRepositoryImpl exclusion regex?

I think with the exclusion of *.inc.yaml it should be enough. Adding more subdirectories to exclusion may potentially cause confusion.

You can of course structure your folder as ./includes/foo.inc.yaml if you wish.

So that you can have ${xxx} without being interpolated?

I understand. I guess it's quite unexpected that " and ' quoted strings behave differently.

This is consistent with sh / bash / ruby, e.g.

$ A=test
$ echo "$A"
test
$ echo '$A'
$A

Maybe we can add the possibility to escape variable expansion with \${...} later

I thought of this, but it's a can of worm that I'd rather not have to deal with yet.

@spacemanspiff2007

Copy link
Copy Markdown
Contributor

__PATH__ is now __DIRECTORY__ although I was tempted to shorten it to __DIR__. It was already done in a commit prior to your last message.

I saw it after I sent the message. Both names are fine with me.

the package keys not being used bothers me a bit. Perhaps it should just be a list instead of a map, but I went with a map to make it all consistent with the rest of the convention for things, items, tags.

Perhaps I can pass an implicit variable ${PACKAGE_KEY} into the included file to make it useful.

For items and things the key is the unique identifier. Since these packages are fully merged configs they have no unique identifier (which would be the path to the file). If you find a good use case for the PACKAGE_KEY then a mapping is okay, otherwise I would prefer a list.

Will you provide support for the short syntax e.g. !include esphome-light.inc.yaml?

I'll consider it.

I thought some more and it might make sense to keep the !include directive.
File exclusion is done by matching the .inc.yaml so the !include should be kept since it aligns the the inc part in the filename.

I think packages is a good name. The idea originally came from https://esphome.io/components/packages.html

Yes - I noticed how everything is inspired by esphome.
However I'm not fully sold on packages, maybe just merge?
Since that's what it does - it merges the configs together.


Will a change in a package/included file trigger a reload of the referencing file?

@jimtng

jimtng commented May 27, 2025

Copy link
Copy Markdown
Contributor Author

the package keys not being used bothers me a bit. Perhaps it should just be a list instead of a map, but I went with a map to make it all consistent with the rest of the convention for things, items, tags.
Perhaps I can pass an implicit variable ${PACKAGE_KEY} into the included file to make it useful.

For items and things the key is the unique identifier. Since these packages are fully merged configs they have no unique identifier (which would be the path to the file). If you find a good use case for the PACKAGE_KEY then a mapping is okay, otherwise I would prefer a list.

It would be entirely up to the user to use it how they see fit. For me, I would use the PACKAGE_KEY as the thingid in the example above so there is no need to specify thingid vars.

Will you provide support for the short syntax e.g. !include esphome-light.inc.yaml?

!include supports short syntax, so yes, that's automatically possible in the packages section too. However, a plain include like that isn't very useful because you could just as easily turn that into a full fledged yaml file on its own (with version: xx) and have it be loaded just like any other normal yaml file.

I thought some more and it might make sense to keep the !include directive.
File exclusion is done by matching the .inc.yaml so the !include should be kept since it aligns the the inc part in the filename.

That is true, although that wasn't the main reason. Using !include is a "free" operation courtesy of the main/generic !include implementation for other purposes. All the packages section is concerned about is merely how to merge the (already included) packages.

I've just tried implementing what you suggested and it resulted in more complexity and code duplication. This is of course simply an implementation detail invisible to the user.

Will a change in a package/included file trigger a reload of the referencing file?

Not currently, however I did consider this possibility. There are pros and cons to doing this, and I opted not doing this, at least for now. The cons: sometimes I might want to make changes to several templates without having it reloaded, and then I can just trigger one reload of the main yaml file. But perhaps it's fine for it to reload several times too.

@jimtng

jimtng commented May 27, 2025

Copy link
Copy Markdown
Contributor Author

However I'm not fully sold on packages, maybe just merge?

I think packages is a more apt term. It means that you can package a device elements (things, items, custom tags) into one file.

merge is more "generic" in meaning, and it doesn't convey the meaning that it's a way to create a cohesive unit like the term package does.

@Nadahar

Nadahar commented May 27, 2025

Copy link
Copy Markdown
Contributor

Instead of packages how about we name the top level key templates

templates is a OH concept (not sure that it will ever end up in these configuration files, but still..) so I would leave that alone.

I guess it's quite unexpected that " and ' quoted strings behave differently.

I think it's rather common that these have different "rules" when it comes to substitutions, escaping etc. I'm not weighing in on whether it's a good idea here, I'm just saying that generally I think that is acceptable. But, doesn't YAML already have some "rules" regarding quoting to take into account? As far as I know, YAML only allows gives the escape character \ "special meaning" in double quotes, which means that you have to use double quotes if you want to include a character that needs escaping.

although I was tempted to shorten it to __DIR__.

That's preferable IMO. When making such "special codes", you might as well make them easy to use.

but I went with a map to make it all consistent with the rest of the convention for things, items, tags.

Don't. It shouldn't become "a mantra" that is more important than reason. People will have to learn to use arrays too, and it's the only thing that makes sense here.

However I'm not fully sold on packages, maybe just merge?

In reality the terms matter less than we think, as soon as something is chosen and its meaning is well-defined and documented, people will quickly "get used to it". If the similarity with the ESPHome format is kept, parity with it might have a value in itself (for users using both systems).

@spacemanspiff2007

spacemanspiff2007 commented May 27, 2025

Copy link
Copy Markdown
Contributor

!include supports short syntax, so yes, that's automatically possible in the packages section too. However, a plain include like that isn't very useful because you could just as easily turn that into a full fledged yaml file on its own (with version: xx) and have it be loaded just like any other normal yaml file.

Your right about the !include in packages.
But how about reusing parts of the configuration like this:

items:
  my_item_name: !include energy_item.inc.yaml
  my_other_name: !include energy_item.inc.yaml

energy_item.inc.yaml:

label: Energy
format: "%.1f"
unit: "kWh"
...

So this will be supported?

I've just tried implementing what you suggested and it resulted in more complexity and code duplication. This is of course simply an implementation detail invisible to the user.

Let's keep the !include then

Not currently, however I did consider this possibility. There are pros and cons to doing this, and I opted not doing this, at least for now. The cons: sometimes I might want to make changes to several templates without having it reloaded, and then I can just trigger one reload of the main yaml file. But perhaps it's fine for it to reload several times too.

But imho this would work the same way if you need to do changes now in an items and things file.
You edit the files outside the folder and then move them into the openhab config folder.
It's unexpected if some file edits will be picked up by openHAB and other won't.

If I reuse an .inc.yaml file in multiple files I have to remember which files to reload when I do a change.
Do you think it would be much effort to implement a reload when the include file changes?

@jimtng

jimtng commented May 27, 2025

Copy link
Copy Markdown
Contributor Author

Your right about the !include in packages. But how about reusing parts of the configuration like this:

items:
  my_item_name: !include energy_item.inc.yaml
  my_other_name: !include energy_item.inc.yaml

energy_item.inc.yaml:

label: Energy
format: "%.1f"
unit: "kWh"
...

So this will be supported?

I haven't tried this exact scenario but yes it should work just like you expected. That's essentially how !include works in all cases.

Not currently, however I did consider this possibility. There are pros and cons to doing this, and I opted not doing this, at least for now. The cons: sometimes I might want to make changes to several templates without having it reloaded, and then I can just trigger one reload of the main yaml file. But perhaps it's fine for it to reload several times too.

But imho this would work the same way if you need to do changes now in an items and things file. You edit the files outside the folder and then move them into the openhab config folder.

I think it depends. Besides, most people edit files in-place.

It's unexpected if some file edits will be picked up by openHAB and other won't.

The two files are different. One is an include file, the other is the main file.

If I reuse an .inc.yaml file in multiple files I have to remember which files to reload when I do a change.

Yes, exactly the idea, but I do see the convenience of having it auto reload and on the other hand, it can be annoying for those who like total control that eventually someone would ask for the option to turn it off. I am so used to doing it manually and the way I currently do it is using one big main file, so I only need to reload one file.

Do you think it would be much effort to implement a reload when the include file changes?

It would be an effort yes, and I am currently on vacation with very limited "computer" time until end of June. This will have to be a separate PR later.

but I went with a map to make it all consistent with the rest of the convention for things, items, tags.

Don't. It shouldn't become "a mantra" that is more important than reason. People will have to learn to use arrays too, and it's the only thing that makes sense here.

It's not merely following a mantra. I actually much prefer to have it as a map. But supporting a list is easy(easier), I'll see what it looks like and try it and if it looks OK, I'll consider supporting both syntaxes perhaps.

@Nadahar

Nadahar commented May 27, 2025

Copy link
Copy Markdown
Contributor

Yes, exactly the idea, but I do see the convenience of having it auto reload and on the other hand, it can be annoying for those who like total control that eventually someone would ask for the option to turn it off. I am so used to doing it manually and the way I currently do it is using one big main file, so I only need to reload one file.

I think the only sensible thing is that the templates "hot reload" too, otherwise you'd have to figure out and chase down which files are affected. I mean, the point of using a template is to avoid duplicating information. If you still have to hunt down the individual files and resave them, you could almost might as well just have copy/pasted.

But, I guess that this can be a challenge implementation-wise. You would have to keep track of basically all "YAML model" files, and then trigger reparsing when required. It sounds like a path full of pitfalls.

@jimtng

jimtng commented May 27, 2025

Copy link
Copy Markdown
Contributor Author

As I said, it depends on your workflow. I've been using a similar system and workflow in openhab for at least three years and preferred manual reloading.

I have an implementation in mind on how to do the auto reload but will also need to add a setting not to do it for those who prefer not having it. I also have in mind how to do this

A.global option through service config
B. Granular option through include Param (least preferred)

I'll implement this in a late pr if/when time permits or at the end of June.

@jimtng jimtng force-pushed the yaml-preprocessor-secretless branch from 0012f04 to 7743285 Compare May 28, 2025 00:24
@jimtng

jimtng commented May 28, 2025

Copy link
Copy Markdown
Contributor Author

Everybody was tired today and returned early, so I had some time to implement the include dependency tracking.

Now whenever any include files are changed, the main yaml will be reloaded.

@jimtng

jimtng commented Feb 7, 2026

Copy link
Copy Markdown
Contributor Author

What happens if you paste YAML into the "Code" tab in the UI? Won't the same processing take place?

No, the preprocessor is applicable to files only

@spacemanspiff2007

Copy link
Copy Markdown
Contributor

@jimtng
I've accidentally stumbled across @ and @@ by looking at the docs.
Are you sure it's a good idea?
We already have ${OPENHAB_CONF} so @ is redundant and it feels like too much magic.
My concern is that especially @@ promotes obscure structures since it references a different file depending on the including file. It "pretends" it's an absolute path when in reality it's not.

@jimtng

jimtng commented Feb 7, 2026

Copy link
Copy Markdown
Contributor Author

I've accidentally stumbled across @ and @@ by looking at the docs.

I've just added it.

Are you sure it's a good idea?

Yes.

We already have ${OPENHAB_CONF} so @ is redundant and it feels like too much magic.

I find it very cumbersome to use !sub ${OPENHAB_CONF}. The @ syntax does not need !sub, nor is it related to it. It's a separate special handling.

My concern is that especially @@ promotes obscure structures since it references a different file depending on the including file. It "pretends" it's an absolute path when in reality it's not.

I don't understand your point. Please elaborate.

@jimtng

jimtng commented Feb 7, 2026

Copy link
Copy Markdown
Contributor Author

@spacemanspiff2007

Copy link
Copy Markdown
Contributor

I find it very cumbersome to use !sub ${OPENHAB_CONF}. The @ syntax does not need !sub, nor is it related to it. It's a separate special handling.

Isn't that rather an argument against !sub and that it's to hard to use?

@spacemanspiff2007 you may not be aware of where this idea came from: https://www.geeksforgeeks.org/javascript/what-does-the-mean-inside-an-import-path-in-vuejs/

Thanks for the background info.

Yes.

I guess we disagree then 😉 .

There should be one way to do something and we already have ${OPENHAB_CONF}.
If that is too hard to use then we should make it easier to use and not add something new to patch it up.

It's very unexpected that !include @/my_file.inc.yaml doesn't include from a folder "@" next to the file.
We're actually dealing with file paths and not vue imports here.
Nowhere else behaves a file path like that and @/my_file.inc.yaml is a valid relative path.

It's a very opinionated implementation and I really don't like it because it mixes file paths with something different without being clear about it.

@jimtng

jimtng commented Feb 7, 2026

Copy link
Copy Markdown
Contributor Author

I find it very cumbersome to use !sub ${OPENHAB_CONF}. The @ syntax does not need !sub, nor is it related to it. It's a separate special handling.

Isn't that rather an argument against !sub and that it's to hard to use?

No. It's an argument against typing out OPENHAB_CONF.

There should be one way to do something and we already have ${OPENHAB_CONF}.

How Pythonic!

I am not good with words, and it's once again almost 5am, so I asked Copilot to create an explanation:

Response to Concerns About the @ Path Alias

Thanks for the feedback — I understand the concern about introducing “another way” to reference paths.
Here’s the reasoning behind the @ prefix and why it isn’t intended to replace ${OPENHAB_CONF}, but to solve a different usability problem.


1. ${OPENHAB_CONF} already exists, but it’s not ergonomic

While ${OPENHAB_CONF} is technically correct, it has several practical drawbacks:

  • It’s long and visually noisy.
  • It’s not discoverable for new users.
  • It blends in with other variables, reducing readability.
  • It forces users to wrap paths in !sub, adding even more syntax.
  • It makes documentation examples harder to follow.

Compare:

file: !sub ${OPENHAB_CONF}/yaml/includes/device.inc.yaml

(Personal note: furthermore, you cannot do this in the scalar include form without having to have a !sub in the parent node)

vs.

file: "@/includes/device.inc.yaml"

Both resolve to the same place, but one is dramatically easier to read, write, and teach.

Many ecosystems introduce shorthand path aliases for exactly this reason — not because the long form is impossible, but because it’s cognitively heavy.


2. openHAB has never been a “one way only” system

The idea that “there should be one way to do something” doesn’t match openHAB’s design philosophy.
The platform already supports multiple approaches for:

  • defining rules (DSL, UI, Blockly, scripts)
  • defining items (files, UI, YAML)
  • defining variables (!sub, inline, includes, environment variables)
  • loading configuration (UI, files, automation)

openHAB has always been pragmatic rather than minimalist.
The @ prefix follows that tradition: it’s a convenience, not a competing mechanism.


3. @/path is not intended to be a literal filesystem path

The concern that @/my_file.inc.yaml “should” refer to a literal folder named @ assumes that include paths are raw filesystem paths.
They aren’t.

The YAML include system already introduces synthetic constructs:

  • !include
  • !sub
  • ${...} variable interpolation
  • ${__DIRECTORY__}
  • ${__FILE_NAME__}
  • ${ENV:FOO}

None of these are literal filesystem semantics.
They are path‑resolution operators inside a domain‑specific configuration language.

@ fits naturally into that model.
It’s not pretending to be a real folder — it’s a symbolic prefix that resolves to ${OPENHAB_CONF}.


4. The mental model is consistent with other ecosystems

Many configuration systems use @ or similar prefixes to denote project‑root or config‑root paths:

  • VS Code
  • Webpack
  • Nuxt
  • TypeScript path aliases
  • Node module resolution
  • Bash (~)
  • Python (. and ..)

These aren’t literal filesystem paths either — they’re syntactic conveniences that improve clarity.


5. The goal is clarity and readability, not patching

The intent isn’t to “patch up” ${OPENHAB_CONF}.
It’s to provide a clean, readable shorthand for the most common case:
referencing files inside the openHAB configuration tree.

${OPENHAB_CONF} remains fully supported.
@ simply makes the common case easier and the documentation clearer.


Closing Thought

If we judge the feature not by ideology (“one way only”) but by practicality, readability, and user experience, the @ prefix is a small addition that yields a large improvement in clarity — especially for new users reading examples.

@jimtng

jimtng commented Feb 7, 2026

Copy link
Copy Markdown
Contributor Author

I'm finding myself writing "@@pkg/xxxxx" and that's still one extra character to type and it looks weird to me.

Considering that

  • for me the most common use case for now is including files that are also in conf/yaml
  • For now I don't need to include CONF/other
  • Includes outside conf/yaml won't (currently) be monitored for changes anyway

I will change it to:

  • DROP the shorthand for OPENHAB_CONF for now
  • change @ to be the alias for conf/<yaml> instead
  • drop the @@ alias

@Nadahar

Nadahar commented Feb 7, 2026

Copy link
Copy Markdown
Contributor

It's a very opinionated implementation and I really don't like it because it mixes file paths with something different without being clear about it.

I don't get that at all. Various kinds of "convenience syntax" is nothing new, you could argue that this whole PR is just that. You can achieve the same through YAML without it, but it might be a lot more to type. As long as they are possible to escape, and the information about how to use and escape them isn't too hard to find, I think it's fine.

That said, I never read the documentation, so I don't know what the intention was for @ vs @@. But, aren't ../.. etc. supported just by being normal file paths? In that sense, you can relatively refer to neighboring folders without it. I would assume that "current dir" is where the file itself resides.

@jimtng

jimtng commented Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

That said, I never read the documentation, so I don't know what the intention was for @ vs @@. But, aren't ../.. etc. supported just by being normal file paths? In that sense, you can relatively refer to neighboring folders without it. I would assume that "current dir" is where the file itself resides.

I asked copilot to further explain this for me:


While standard relative paths (e.g., ../../) are technically supported, the @ syntax—which has been streamlined to serve as the single root-relative indicator—is designed to address user experience and configuration maintainability in ways that standard paths cannot:

  • Universal Addressing (Zero Mental Math): Standard relative paths require the author to manually calculate the distance between the current file and the target. By using @ to denote the project root (CONF/yaml), a statement like !include "@/includes/device.inc.yaml" becomes a universal address. It works identically regardless of which file it is pasted into, removing the need to "figure out" the folder depth.
  • Portability and Refactor Resilience: Relative paths (../) create "fragile links." If a user moves a configuration file to a different sub-directory to reorganize their project, every relative !include statement breaks. The @ syntax ensures that configurations are location-independent; a file can be moved anywhere in the hierarchy without its internal references breaking.
  • Shorthand for Environment Variables: While this could be achieved using full paths or environment variables (e.g., ${OPENHAB_CONF}/yaml/...), the @ symbol provides a clean, standardized shorthand that improves readability while maintaining the same absolute reference stability.

In short, while ../ is a direction, @ is a location. It transforms a spatial puzzle into a stable, readable reference that is much easier to maintain as a project grows.


It seems that I need to provide further explanation and examples in the docs about this.

@jimtng

jimtng commented Feb 8, 2026

Copy link
Copy Markdown
Contributor Author
  • @ symbol provides a clean, standardized shorthand that improves readability

That's what copilot/gemini says. Before someone says that ${OPENHAB_CONF} is more readable, let me state again that this is for convenience so I don't have to always type out that long thing everywhere in my files.

You can use ${OPENHAB_CONF}/yaml in yours and we'll all be happy.

Or in the AI speak:


  • Syntactic Convenience: The @ symbol acts as an ergonomic shorthand for the root directory. While the same stability can be achieved using ${OPENHAB_CONF}, this syntax is an intentional convenience to avoid the repetitive overhead of typing out long-form variables across multiple files. It offers a "low-friction" alternative for users who value brevity, while still allowing others to use full environment variables if they prefer.

@Nadahar

Nadahar commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

In short, while ../ is a direction, @ is a location. It transforms a spatial puzzle into a stable, readable reference that is much easier to maintain as a project grows.

This is a "two-way-street", because with relative paths, you can move things around as you want to, as long as you move them together. With fixed paths, that's not the case. I agree that "location shortcuts" are probably more useful here, most of the time, but that also depends on how one organizes the files. I've come to generally think of relative paths a "king" in most circumstances, because as long as you move the whole file structure, it's "self contained" and will work anywhere.

It seems that I need to provide further explanation and examples in the docs about this.

I don't think you need to explain this, it was just a suggestion as an alternative to having to type "the long version". It depends on the configuration, but if you have a limited number of folders and e.g. one folder for includes, keeping track of the relative paths isn't very complicated. The most flexible is to be able to do both, obviously.

@jimtng

jimtng commented Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

The most flexible is to be able to do both, obviously.

And indeed both are supported.

@jimtng

jimtng commented Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

I don't think you need to explain this

This has been an eye-opener for me. Because I have been so deep in the development of this feature, I overlooked the fact that certain design choices weren't as self-evident to a first-time reader as they were to me. "Your" (collective) feedback is very valuable because it highlighted exactly where the narrative thread was missing; I now see that providing this explicit context is necessary to bridge that gap.

@spacemanspiff2007

spacemanspiff2007 commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

@jimtng you love to copy paste copilot answers but I've found that almost always they are not correct at all or look superficially good but are not correct in detail.
I would really appreciate if you took the time to formulate and write your own opinion and thoughts.
After all I'm talking to you and not copilot.


I'll address some arguments this time:

1. ${OPENHAB_CONF} already exists, but it’s not ergonomic

It’s long and visually noisy.

Valid argument, maybe an additional short alias like OH_CONF would be better

It’s not discoverable for new users.

Neither is the @ operator because it's an openhab specific implementation

It blends in with other variables, reducing readability.

It's a variable expansion so of course it blends in.

It forces users to wrap paths in !sub, adding even more syntax.

It was an explicit decision to make !sub required so the arguments in favor of that apply here, too.

It makes documentation examples harder to follow.

Absolutely invalid since the documentation does not refer to the openHAB folder structure

2. openHAB has never been a “one way only” system

defining items (files, UI, YAML)

The yaml format is a file so there exactly two ways to define things: UI and files

defining variables (!sub, inline, includes, environment variables)

What is inline in the yaml context? !sub is something totally different than !include. Environment variables have a different scope than file variables and can not used interchangeably. This makes no sense.

loading configuration (UI, files, automation)

I can load openHAB configuration (services / addons) through files or UI but not through automation.

3. @/path is not intended to be a literal filesystem path

include paths are raw filesystem paths. They aren’t.

They actually are. After the variable expansion the include path is expected to be a valid path - relative or absolute.

4. The mental model is consistent with other ecosystems

TypeScript path aliases

These are aliases that are configurable by the user so they are something totally different.
If you suggest these aliases are configurable it's another discussion.

Bash (~)

The user home is not project specific but works system wide

Python (. and ..)

This is just a relative import which would be the equivalent to ./file.inc.yaml and ../file.inc.yaml.
The statement is plain wrong

These aren’t literal filesystem paths either — they’re syntactic conveniences that improve clarity.

Bash ~ is literal a file system path. In python the import is for modules and packages so something totally different.

5. The goal is clarity and readability, not patching

It’s to provide a clean, readable shorthand for the most common case:
referencing files inside the openHAB configuration tree.

That's literally the only case for this feature. Which also works well with relative paths.

@ simply makes the common case easier and the documentation clearer.

Again - the @ does not appear in the docs so that's a lie.

Closing Thought

@ prefix is a small addition that yields a large improvement in clarity —

I find it very confusing, especially the @@ operator so for me the does not improve clarity but is yet another feature I have to learn.

especially for new users reading examples.

Again not true.


I tried to address some arguments and explain why I think they are void and I hope you see that these are not good arguments, too.
I've found your work and implementation very good and thought through and if you present AI slop as your own thoughts you're really selling yourself under value.


change @ to be the alias for conf/ instead

Just to play devils advocate:
I really like the CONF/items folder and I think the @ shortcut should point to it.
Why is your preference worth more than mine?

Includes outside conf/yaml won't (currently) be monitored for changes anyway

I thought items and things and the other subfolders are monitored, too?
If we load the yaml files from there I would expect to work it in the same way.


I've come to generally think of relative paths a "king" in most circumstances, because as long as you move the whole file structure, it's "self contained" and will work anywhere.

I agree. Relative paths are great because they allow the creation of self sustained packages which can be moved around.


As a suggestion:

  1. How about we either
  • allow the user to configure the own aliases which then can be referenced with @<ALIAS_NAME>
  • Use @ + conf/<subfolder> name, e.g. @yaml refers to conf/yaml, @items refers to conf/items, @things refers to conf/things, etc.
  1. Add a config setting for global variables which are automatically available in all files. That way one could write
    !include !sub {MY_LIB}/file_1.inc.yml where the variable ${MY_LIB} is defined globally.

and it's once again almost 5am

You should really get more sleep. Openhab and these issues can definitely wait a little longer for your reply.

@spacemanspiff2007

spacemanspiff2007 commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

An exaggerated summary:
@jimtng : "Interpolation everywhere is dangerous - we should make the user write !sub to be extra explicit"
Also @jimtng : "I don't want to write this everywhere!"
😆

Just to be extra clear: this is meant as a humorous joke and not as an attack. I'm aware of the different nuances and subtle differences.

@jimtng

jimtng commented Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

It is clear we have different opinions and preferences.

The way it is implemented right now, @ points to CONF/XXX/ where

  • XXX is yaml if your main file lives in CONF/yaml/any/where
  • XXX is items if your main file lives in CONF/items/any/where

If your directory is literally called @ you can do !include ./@/myfile.inc.yaml or even !include ../../../@/myfile.inc.yaml. That will work just fine too.

  • You are free to use relative paths if that's your preference. It's supported.
  • You can use absolute paths
  • You can use substitutions
  • You can use @ as described above

You should really get more sleep.

On this, I totally agree. BUT - I really want to get this done and move on to other things.

@Nadahar

Nadahar commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

It was an explicit decision to make !sub required so the arguments in favor of that apply here, too.

I think that is mixing the cards a bit. !sub is about general substitution "anywhere" in the document. This is about a shorthand that is "expanded" in one very specific situation and location. Also, !sub isn't "required" since you can do that at the root level, so I don't get why that is still a subject.

I can load openHAB configuration (services / addons) through files or UI but not through automation.

You can do it programmatically using scripts/rules, which is probably what is meant by "automation" here. Not a very convenient way to do it, but theoretically possible.

  • Use @ + conf/<subfolder> name, e.g. @yaml refers to conf/yaml, @items refers to conf/items, @things refers to conf/things, etc.

I thought it already worked with "the current subfolder", but in any case - if let's say that @ points to conf/, couldn't you just write @/yaml, @/items etc. instead? That / is a really tiny difference, and it is "standard" and doesn't require further "magic" that must be learned/found to exist.

Generally on the use of LLMs, I'm a huge skeptic and generally don't like using them. The reason is that they are only good at making things appear correct superficially, and usually most of the details aren't correct with further scrutiny. I did a fair bit of image generation using SDXL, and it has the exact same problem - images can look amazing, but when you look at the details, it's almost always wrong in some way. I'm no LLM expert, but I'm skeptical to the claim that this is just about "tuning", I think it might be about something fundamental to the whole approach. As I understand it, the whole thing is built upon making something that "appears correct". That is the criteria that is used when iterating and making improvements to the result. The details are never in focus, and I'm not sure that a LLM can ever produce correct details.

Therefore, I use it very sparingly. I have used it for writing some documentation in a couple of instances, but that is more "as an inspiration" for the general layout and what to include. I've then gone through the whole thing, correcting things, and I've usually ended up rewriting more or less every sentence. As such, I'm not sure that it does me any favors all in all, it might have been better if it just suggested which sections to include/what headlines to use, and I wrote the content myself.

I therefore share the sentiment that we should limit the use of LLM output as "arguments", because what you really do then, is dump the "cognitive load" of dissecting all the half-truths on your opponent. Which is what you'd want to do in a war, but not something that is "nice" in a friendly discussion 😉

@jimtng

jimtng commented Feb 8, 2026

Copy link
Copy Markdown
Contributor Author

I thought it already worked with "the current subfolder", but in any case - if let's say that @ points to conf/, couldn't you just write @/yaml, @/items etc. instead? That / is a really tiny difference, and it is "standard" and doesn't require further "magic" that must be learned/found to exist.

The / after @ is optional - makes no difference. You can write it as @/pkg/xxx or @pkg/xxx.

There's no point in typing @yaml hundreds of times when it's always going to be the same in my case. It's not going to ever be different unless you are spreading your YAML files across different top-level dirs (CONF/yaml|items|tags|things) and you are cross-referencing them from one directory to another.

If you are NOT cross-referencing (e.g. including CONF/item/xxx from CONF/yaml/xxx) them, then it's a moot point to discuss this.

I am not sure if both of you knew this:

  • @ points to CONF/items/ if you're using it within CONF/items/
  • @ points to CONF/things/ if you're using it within CONF/things/
  • @ points to CONF/tags/ if you're using it within CONF/tags/
  • @ points to CONF/yaml/ if you're using it within CONF/yaml/

If you ARE cross referencing files between different dirs, you can use ${OPENHAB_CONF}

If you want something short for ${OPENHAB_CONF} you can always alias it to ${C} at the top of your files and use ${C} throughout that file.

Lastly: if your main file is in CONF/yaml, @../items/anotherfile.inc.yaml will resolve to CONF/items/anotherfile.inc.yaml so that's YET another option for cross referencing.

@spacemanspiff2007

spacemanspiff2007 commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

I am not sure if both of you knew this:

Yes - I understood the feature and your idea.
I still think being a little bit more explicit where the file comes from is much butter.
I'm also still torn about it because I still think that this feature is an admission that !sub and the variable expansion is too complicated and too much effort from the user side.

There's no point in typing @yaml hundreds of times when it's always going to be the same in my case. It's not going to ever be different unless you are spreading your YAML files across different top-level dirs (CONF/yaml|items|tags|things) and you are cross-referencing them from one directory to another.

That's what I was trying to say. You tailored a shortcut especially to your needs.
Maybe the user wants to put his template files in conf/templates and reference them.
I think it's only fair if he gets some convenience, too.
The suggestion can't be to have the user write @/../templates.

Requiring the folder name after the @ makes it explicitly clear where the included file comes from.
It makes it - especially for not so advanced users - much easier to initially understand and easier to maintain later on.
E.g. the path @yaml/my_file.inc.yaml makes it very clear where file comes from.
It's a real absolute path which points everywhere and all the time to the same folder.
The user then could use @templates/my_file.inc.yaml or @items/my_file.inc.yaml which all are very clear and readable.

As for your special use case:
You have already migrated many files to the new format so it's just a (folder) search replace for you.
I'm aware that changing the implementation would make it a tiny bit more inconvenient for you but I still think it's well worth it.

@jimtng

jimtng commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

I can make it customizable to please everyone:

preprocessor:
  path_aliases:
    "@": !sub ${OPENHAB_CONF}

I can even make a "global default" file that applies to all the files in that directory and subdirectories so you only define it once. The sky's the limit.

@jimtng

jimtng commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

so it's just a (folder) search replace for you

That's not a good argument. It is also a search/replace to !sub ${OPENHAB_CONF} but I don't want to do that.

@spacemanspiff2007

spacemanspiff2007 commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

The / after @ is optional - makes no difference. You can write it as @/pkg/xxx or @pkg/xxx.

I think it's very unexpected that @/pkg/xxx is the same file as @pkg/xxx

That's not a good argument. It is also a search/replace to !sub ${OPENHAB_CONF} but I don't want to do that.

That was not an argument but rather intended as a friendly hint that your effort that you poured in your current config files is not lost and there is an easy way forward.

I can even make a "global default" file that applies to all the files in that directory and subdirectories so you only define it once. The sky's the limit.

That's not what I asked and I definitely don't think that's something you should do.
I asked to make this feature less complex and a tiny bit more explicit because it's then both easier to understand and it caters to a wider audience because it satisfies more use cases:

  • It would allow referencing every folder from everywhere while still providing a concise and easy to use shortcut. Not everyone is using conf/yaml for all configuration I think they deserve some convenience, too
  • It would also allow using a dedicated template folder for includes and allow the possibility to reference it from everywhere.

How about allowing @<ROOT_FOLDER_NAME>/my_file.inc.yaml and use @./my_file.inc.yaml as the current root directory (if we must have it)?

PS:
I found your comment to be rather snide, and I don't think that's appropriate, as I have tried to present my arguments objectively and soberly and I have also tried to suggest multiple alternatives in my last posts which could have been discussed as a way forward.

@jimtng

jimtng commented Feb 10, 2026

Copy link
Copy Markdown
Contributor Author

PS:
I found your comment to be rather snide, and I don't think that's appropriate, as I have tried to present my arguments objectively and soberly and I have also tried to suggest multiple alternatives in my last posts which could have been discussed as a way forward.

I apologise.

I would propose the following:

  • @/ => CONF - acts as the fixed anchor, so for items @/items/...
  • $/ => (what @ currently is): the current "subroot" where you're working from e.g. CONF/yaml, CONF/items, etc. - acts as the dynamic "anchor, since $ is often associated with "variables".

This would still give me the brevity that I want.

@spacemanspiff2007

Copy link
Copy Markdown
Contributor

How about we directly include the subfolder name?
E.g. @items/my_file.in.yaml or @yaml/... since it's a little bit more readable.
Then the logic would be @ + <folder_name>.

$ could also be a valid folder name which points to the current sub-root:
E.g. @$/my_file.inc.yaml

That way we have the @ which always denotes a path alias and the $ as a variable for the current sub-root.
What do you think?

@jimtng

jimtng commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author

I've been wondering whether this should go into Core at all, or whether it should be implemented as an add-on.

My thinking is that this is merely a secondary level of "convenience" and not in itself adding an actual core "feature". In other words, core can work fine without this PR. My concern about adding it to core is that it adds an extra level of complexity and also increase memory requirements (bloat) even if it's only a few KB/MB (I haven't measured).

I'm thinking if added as an addon it can monitor CONF/yamlx (?) and generate the "compiled" final output to CONF/yaml, which core can then load as usual. It means that once it has compiled the output, its job is done and it can even be uninstalled without affecting core.

I haven't tried to turn this into an addon but it will be next on my todo list.

The only downside I can see is the multi-page documentation will need to live somewhere. For now it can go into ./docs. There are several options to improve this:

  • Publish into ghpages
  • Integrate multi-page doc (beyond just the README.md) from addons intoopenhab.org/addons/xxx/docs

@Nadahar

Nadahar commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

I would, if you go down that path, think about whether it would be better to create a "hook" in the YAML parser in core that you can plug into, or if it's better to just work with files as you suggest. I'm not sure what the consequences will be, how it will affect the actual use, but with a hook, one would open up for potentially also other "add-ons" that could pre-process the content in various ways. I'm not sure that there's actually a "need" for that, but it should probably be evaluated what is "the best arrangement".

The argument as to whether it is "part of core or not" could be made about many things. The YAML parser itself could have been made as an add-on, the same could e.g. the DSL parsers. It's not always clear what belongs where IMO, but whatever "comes as standard" is more likely to see widespread use, I would think.

@jimtng

jimtng commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author

As an add-on, I'm not sure which category it should fit into? System integration maybe?

@Nadahar

Nadahar commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

As an add-on, I'm not sure which category it should fit into? System integration maybe?

Yes, it's the IO/misc category, sometimes called "system integration". There's nothing else really, unless you consider "automation". But, "automation" in OH actually means rules/scripts - which it isn't.

@jimtng

jimtng commented Feb 27, 2026

Copy link
Copy Markdown
Contributor Author

I've got a working add-on now. I'll close this PR and submit one to the add-on repo when it's ready. It will be called org.openhab.io.yamlcomposer

It will also be easier to test with a simple addon jar without requiring any changes to core.

It monitors CONF/yamlsrc/ and writes the generated output into CONF/yaml/composed/. I think this arrangement is much better because it keeps the CONF/yaml directory really clean - no includes/package files, just the final yaml output to be loaded by openhab.

Thanks again for all the feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement An enhancement or new feature of the Core

Projects

None yet

Development

Successfully merging this pull request may close these issues.