Home Assistant Elixir
View Sourcehome_assistant_elixir is an embedded Elixir DSL for declaring Home Assistant entities and automations, then materializing those declarations as YAML files ready to be used by Home Assistant.
Installation
Add home_elixir to your dependencies in mix.exs:
def deps do
[
{:home_elixir, "~> 0.1.0"}
]
endConfiguration
In your config/config.exs:
config :home_assistant_api_client,
url: "http://YOUR_HOME_ASSISTANT_URL:8123",
token: "YOUR_LONG_LIVED_ACCESS_TOKEN"
config :home_elixir,
entity_folder: "/path/to/your/home_assistant/config/",
automation_folder: "/path/to/your/home_assistant/config/"The entity_folder and automation_folder must point to your Home Assistant config/ directory. The DSL writes YAML files there at compile time.
:home_assistant_api_client is a separate package (hex.pm/packages/home_assistant_api_client) that home_elixir depends on for the runtime get_*/set_*/restart_home_assistant/0 helpers described below — it's pulled in automatically, you don't need to add it to your own deps.
To generate a long-lived access token, go to your Home Assistant profile page (/profile), open the Security tab, and scroll to Long-Lived Access Tokens.
How it works
Declaring entities and automations happens at compile time; writing the YAML files happens at runtime, through a function call you make explicitly:
- Define one or more Elixir modules with
use HomeElixir.DSL. - Inside those modules, declare
entityandautomationblocks. - During compilation, the DSL macros accumulate those declarations in module attributes.
- Right before the module finishes compiling,
__before_compile__/1transforms the collected declarations into an intermediate representation and injects agenerate_home_assistant_config!/0function into the module — it does not write any files itself. - At runtime (app start, a mix task,
iex), callYourModule.generate_home_assistant_config!(). That's when the intermediate representation is compiled into Elixir maps and written as YAML files in the configured Home Assistant folders.
Generation is no longer driven by an external ProjectGenerator.generate/1 call — but it's also not automatic on compile. Calling the compiler/writer from inside __before_compile__/1 used to be the behavior, but it raced against the compilation of other referenced modules and failed intermittently, so it was moved to an explicit runtime call.
What it generates
The compiler writes one YAML file per declared entity, inside a folder named after its domain:
input_boolean/<entity_id>.yamlinput_number/<entity_id>.yamlinput_select/<entity_id>.yamlalarm_control_panel/<entity_id>.yaml
And one YAML file per declared automation, named after its alias:
automations/<alias>.yaml
All of these live directly under the entity_folder/automation_folder you configured (see Configuration) — there is no intermediate entities/ directory.
Required Home Assistant structure
Home Assistant loads these folders through !include_dir_merge_named/!include_dir_merge_list directives in configuration.yaml, and it checks that the target directory already exists before it cares whether it's empty. So the folders below must exist as placeholders in your Home Assistant config/ directory before the very first compile — otherwise Home Assistant refuses to start, and the DSL never gets the chance to create them for you.
config/
├── input_boolean/
├── input_number/
├── input_select/
├── alarm_control_panel/
├── automations/
└── configuration.yamlCreate them upfront, even empty:
mkdir -p config/{input_boolean,input_number,input_select,alarm_control_panel,automations}
Git does not track empty directories, so if your Home Assistant config/ is under version control, add a placeholder file (e.g. .gitkeep) inside each one so the folders ship with the repo:
touch config/{input_boolean,input_number,input_select,alarm_control_panel,automations}/.gitkeep
Then wire them up in configuration.yaml:
input_boolean: !include_dir_merge_named input_boolean/
input_number: !include_dir_merge_named input_number/
input_select: !include_dir_merge_named input_select/
alarm_control_panel: !include_dir_merge_list alarm_control_panel/
automation: !include_dir_merge_list automations/From then on, compiling your DSL modules fills these folders with one YAML file per entity/automation. File.mkdir_p!/1 recreates each domain folder on every write, but Home Assistant needs it to already be there from the start.
Configuration
Set the output folders in:
home_assistant_elixir/config/config.exsYou must configure:
:entity_folder:automation_folder
Those paths must point to the config/ directory used by Home Assistant.
How to organize modules
The recommended shape is:
- one or more DSL modules grouped by domain
- each module should contain only declarations written with the DSL
- compiling those modules triggers YAML generation
For example:
defmodule MyHome.Alarm do
use HomeElixir.DSL
entity :input_boolean, "alarm_enabled" do
name "Alarm enabled"
initial false
end
end
defmodule MyHome.Covers do
use HomeElixir.DSL
entity :input_select, "living_room_cover" do
options ["open", "close"]
initial "close"
end
endWhy this shape
This fits the current architecture well:
- each DSL module is a declaration unit
- the macros collect declarations locally in that module
__before_compile__/1compiles only what that module declared- the compiler/writer layer is responsible for materialization
Minimal example
This is a small end-to-end example with:
- one
input_boolean - one automation reacting to a state change
- one notification action
defmodule Demo.Switches do
use HomeElixir.DSL
entity :input_boolean, "test_switch" do
name "Test switch"
initial false
end
automation "notify when test switch turns on" do
trigger do
state do
affected_entities [{:input_boolean, "test_switch"}]
to true
end
end
action do
persistent_notification "create" do
message "The test switch is on"
end
end
end
endWhen Demo.Switches is compiled, the DSL will:
- register the declared entities and automation steps in module attributes
- build the automation intermediate representation
- compile entities and automations into Elixir maps
- write the resulting YAML files to the configured folders
How the DSL works internally
Each module that uses HomeElixir.DSL gets:
- module attributes used to accumulate declarations
- macros for entities, triggers, conditions and actions
- an internal
__before_compile__/1hook - generated helper functions
The process is:
entitydeclarations are stored in type-specific attributes such as@input_booleanor@input_number.automationdeclarations are recorded as an ordered list of steps in@automation_steps.__before_compile__/1reads those attributes and reorganizes them into a structured intermediate representation.- The compiler layer turns that representation into domain maps.
- The YAML writer serializes those maps and persists them on disk.
Embedded helper functions
When a module uses HomeElixir.DSL, the DSL also injects helper functions into that module.
There are two groups of generated helpers:
- internal introspection helpers that expose the declarations collected during compilation
- client helpers that call
HomeAssistantApiClient
Introspection helpers
These functions are generated automatically:
__home_elixir_entities__/0__home_elixir_automations__/0
Example:
Demo.Switches.__home_elixir_entities__()
Demo.Switches.__home_elixir_automations__()They are mainly useful for debugging and for inspecting what the DSL compiled internally.
Client helpers generated per entity
For every declared entity, helper functions are generated in the same module so you can interact with Home Assistant through HomeAssistantApiClient.
For input_boolean:
entity :input_boolean, "test_switch" do
initial false
endThis injects:
get_input_boolean_test_switch/0set_input_boolean_test_switch/1
Example:
Switches.get_input_boolean_test_switch()
Switches.set_input_boolean_test_switch(:turn_on)
Switches.set_input_boolean_test_switch(:turn_off)For input_number:
entity :input_number, "temperature_limit" do
min 10
max 30
endThis injects:
get_input_number_temperature_limit/0set_input_number_temperature_limit/1
Example:
Settings.get_input_number_temperature_limit()
Settings.set_input_number_temperature_limit(22)For input_select:
entity :input_select, "cover_mode" do
options ["open", "close"]
endThis injects:
get_input_select_cover_mode/0set_input_select_cover_mode/1
Example:
Covers.get_input_select_cover_mode()
Covers.set_input_select_cover_mode("open")For alarm_control_panel:
entity :alarm_control_panel, "alarm" do
platform :manual
code "1234"
endThis injects:
get_alarm_control_panel_alarm/0set_alarm_control_panel_alarm_state/2
Example:
Alarm.get_alarm_control_panel_alarm()
Alarm.set_alarm_control_panel_alarm_state(:alarm_arm_home, "1234")
Alarm.set_alarm_control_panel_alarm_state(:alarm_disarm, "1234")Restart helper
Every module using the DSL also gets:
restart_home_assistant/0
Example:
Switches.restart_home_assistant()This calls the Home Assistant restart service through the client.
Notes about these helpers
- these functions are generated from the declared entity names
- entity names should therefore be readable and valid for generated function names
- these helpers are runtime convenience wrappers around
HomeAssistantApiClient - the YAML generation path is independent from calling these runtime helpers
Current DSL shape
Entities
Supported entity types:
:input_boolean:input_number:input_select:alarm_control_panel
An entity declaration has three parts: the type (one of the domains
above), the id (second argument — becomes the Home Assistant
entity_id and the generated helper functions' suffix), and everything
else inside the do block, which are optional attributes — name (the
display name shown in the Home Assistant UI) plus type-specific ones like
initial, icon, min/max, options, platform/code, etc. See the
HomeElixir DSL guide for the full attribute list per type.
Examples:
entity :input_boolean, "presence" do
name "Presence"
initial false
end
entity :input_number, "temperature_limit" do
min 10
max 30
initial 20
end
entity :input_select, "cover_mode" do
options ["open", "close"]
initial "open"
end
entity :alarm_control_panel, "alarm" do
platform :manual
code "1234"
endAutomations
The current high-level structure is:
automation "some alias" do
trigger do
...
end
condition do
...
end
action do
...
end
endAvailable trigger forms
state do
affected_entities [{:input_boolean, "presence"}]
to true
end
numeric_state do
affected_entities [{:input_number, "temperature_limit"}]
above 20
end
sun do
event :sunrise
endAvailable condition forms
state_condition do
affected_entities [{:alarm_control_panel, "alarm"}]
state_value "armed_home"
end
logical_condition :or do
state_condition do
affected_entities [{:alarm_control_panel, "alarm"}]
state_value "armed_home"
end
state_condition do
affected_entities [{:alarm_control_panel, "alarm"}]
state_value "armed_away"
end
endAvailable action forms
Generic service action:
service :input_boolean, "turn_on" do
affected_entities [{:input_boolean, "presence"}]
endinput_select service action with option:
service :input_select, "select_option" do
affected_entities [{:input_select, "cover_mode"}]
option "open"
endPersistent notification:
persistent_notification "create" do
message "Hello from HomeElixir"
endNotes
- YAML generation now happens during module compilation, not through an external project generator.
- If you split your DSL across many files, each compiled DSL module can materialize its own declarations.
- The current DSL scope is intentionally small and focused on the entity and automation types already implemented in the compiler.