Home Assistant Elixir

home_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"}
]
end

Configuration

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:

  1. Define one or more Elixir modules with use HomeElixir.DSL.
  2. Inside those modules, declare entity and automation blocks.
  3. During compilation, the DSL macros accumulate those declarations in module attributes.
  4. Right before the module finishes compiling, __before_compile__/1 transforms the collected declarations into an intermediate representation and injects a generate_home_assistant_config!/0 function into the module — it does not write any files itself.
  5. At runtime (app start, a mix task, iex), call YourModule.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:

And one YAML file per declared automation, named after its alias:

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.yaml

Create 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.exs

You must configure:

Those paths must point to the config/ directory used by Home Assistant.

How to organize modules

The recommended shape is:

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
end

Why this shape

This fits the current architecture well:

Minimal example

This is a small end-to-end example with:

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
end

When Demo.Switches is compiled, the DSL will:

  1. register the declared entities and automation steps in module attributes
  2. build the automation intermediate representation
  3. compile entities and automations into Elixir maps
  4. write the resulting YAML files to the configured folders

How the DSL works internally

Each module that uses HomeElixir.DSL gets:

The process is:

  1. entity declarations are stored in type-specific attributes such as @input_boolean or @input_number.
  2. automation declarations are recorded as an ordered list of steps in @automation_steps.
  3. __before_compile__/1 reads those attributes and reorganizes them into a structured intermediate representation.
  4. The compiler layer turns that representation into domain maps.
  5. 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:

Introspection helpers

These functions are generated automatically:

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
end

This injects:

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
end

This injects:

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"]
end

This injects:

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"
end

This injects:

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:

Example:

Switches.restart_home_assistant()

This calls the Home Assistant restart service through the client.

Notes about these helpers

Current DSL shape

Entities

Supported entity types:

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"
end

Automations

The current high-level structure is:

automation "some alias" do
trigger do
...
end
condition do
...
end
action do
...
end
end

Available 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
end

Available 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
end

Available action forms

Generic service action:

service :input_boolean, "turn_on" do
affected_entities [{:input_boolean, "presence"}]
end

input_select service action with option:

service :input_select, "select_option" do
affected_entities [{:input_select, "cover_mode"}]
option "open"
end

Persistent notification:

persistent_notification "create" do
message "Hello from HomeElixir"
end

Notes