Packages

An embedded Elixir DSL for declaring Home Assistant entities and automations, compiled at build time into the YAML files Home Assistant expects.

Current section

Files

Jump to
home_elixir README.md
Raw

README.md

# 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`:
```elixir
def deps do
[
{:home_elixir, "~> 0.1.0"}
]
end
```
## Configuration
In your `config/config.exs`:
```elixir
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](https://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:
- `input_boolean/<entity_id>.yaml`
- `input_number/<entity_id>.yaml`
- `input_select/<entity_id>.yaml`
- `alarm_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](#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.
```text
config/
├── input_boolean/
├── input_number/
├── input_select/
├── alarm_control_panel/
├── automations/
└── configuration.yaml
```
Create them upfront, even empty:
```bash
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:
```bash
touch config/{input_boolean,input_number,input_select,alarm_control_panel,automations}/.gitkeep
```
Then wire them up in `configuration.yaml`:
```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:
```elixir
home_assistant_elixir/config/config.exs
```
You 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:
```elixir
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:
- each DSL module is a declaration unit
- the macros collect declarations locally in that module
- `__before_compile__/1` compiles 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
```elixir
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:
- module attributes used to accumulate declarations
- macros for entities, triggers, conditions and actions
- an internal `__before_compile__/1` hook
- generated helper functions
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:
- 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:
```elixir
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`:
```elixir
entity :input_boolean, "test_switch" do
initial false
end
```
This injects:
- `get_input_boolean_test_switch/0`
- `set_input_boolean_test_switch/1`
Example:
```elixir
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`:
```elixir
entity :input_number, "temperature_limit" do
min 10
max 30
end
```
This injects:
- `get_input_number_temperature_limit/0`
- `set_input_number_temperature_limit/1`
Example:
```elixir
Settings.get_input_number_temperature_limit()
Settings.set_input_number_temperature_limit(22)
```
For `input_select`:
```elixir
entity :input_select, "cover_mode" do
options ["open", "close"]
end
```
This injects:
- `get_input_select_cover_mode/0`
- `set_input_select_cover_mode/1`
Example:
```elixir
Covers.get_input_select_cover_mode()
Covers.set_input_select_cover_mode("open")
```
For `alarm_control_panel`:
```elixir
entity :alarm_control_panel, "alarm" do
platform :manual
code "1234"
end
```
This injects:
- `get_alarm_control_panel_alarm/0`
- `set_alarm_control_panel_alarm_state/2`
Example:
```elixir
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:
```elixir
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.html) guide for the full attribute list per type.
Examples:
```elixir
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:
```elixir
automation "some alias" do
trigger do
...
end
condition do
...
end
action do
...
end
end
```
### Available trigger forms
```elixir
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
```elixir
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:
```elixir
service :input_boolean, "turn_on" do
affected_entities [{:input_boolean, "presence"}]
end
```
`input_select` service action with option:
```elixir
service :input_select, "select_option" do
affected_entities [{:input_select, "cover_mode"}]
option "open"
end
```
Persistent notification:
```elixir
persistent_notification "create" do
message "Hello from HomeElixir"
end
```
## Notes
- 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.