Packages

An Elixir client for the Home Assistant REST API. Provides functions to query entity states, call services, update configurations, and evaluate templates in your Home Assistant instance.

Current section

Files

Jump to
home_assistant_api_client lib home_assistant_api_client.ex
Raw

lib/home_assistant_api_client.ex

defmodule HomeAssistantApiClient do
alias HomeAssistantApiClient.Types
use Corsa
require Logger
@moduledoc """
Client for the Home Assistant home automation platform.
Provides functions to query states, activate services, modify entities, validate
configurations, and evaluate templates.
`entry_point_get/1` and `entry_point_post/2` are the low-level entry points to the API
(GET and POST respectively); every other function in this module is built on top of them.
## GET functions
* `get_config/0` - retrieves the `.yaml` configuration.
* `get_logbook/1` - retrieves the log before the given date, in `"YYYY-MM-DDThh:mm:ssTZD"`
format. If no date is provided, returns the logs from the current day.
* `get_service_domains/0` - returns the domain types.
* `get_service_domain/1` - given a specific domain, returns the full service.
* `get_entities/0` - returns the names of all entities.
* `get_entity/1` - returns the state of the entity with the given name.
* `get_entities_by_type/1` - returns all the entities of a specific type.
* `get_entity_types/0` - returns the types of entities there are.
## POST functions (state changes)
`set_entity_state/2` changes the state of an entity directly:
```
{:ok, _element} = HomeAssistantApiClient.set_entity_state({:input_boolean, "luz_cuarto_gabriel_principal"}, false)
```
`set_service/2` and `set_service/3` call a service for a given domain, optionally with an
extra argument such as a value or option:
```
{:ok, _element} = HomeAssistantApiClient.set_service({:input_number, "temperatura_simulada"}, :set_value, 20)
```
`template_host/1` evaluates a [Home Assistant template](https://www.home-assistant.io/docs/configuration/templating/):
```
{:ok, _value} = HomeAssistantApiClient.template_host("{{ states('sensor.sensor_temperatura_mock') }}")
```
`check_config/0` checks the configuration to ensure everything is correct.
`intent/2` triggers a semantic intent in Home Assistant, simulating a voice query or virtual
assistant action (e.g. `HassTurnOn`, `HassGetState`), with the necessary data so Home Assistant
interprets and responds as if it were a conversational interaction:
```
{:ok, _map} = HomeAssistantApiClient.intent("HassGetState", %{"name" => "sensor.sensor_temperatura_mock"})
```
## Domains
A domain identifies the type of entity or service being addressed (e.g. `:light`,
`:input_boolean`, `:alarm_control_panel`). Throughout this module's public API, a domain is
always an atom — that is what you pass in as an argument, and what functions such as
`get_entities/0` or `get_service_domains/0` return.
Home Assistant's REST API, however, works with domains as strings, since entity ids on the
wire look like `"light.salon"` and JSON responses list domains as `"light"`. This module
converts between the two representations at its boundary with the HTTP layer:
* Outgoing requests: the atom is turned into a string with `Atom.to_string/1` when building
the entity id or service path (e.g. `"\#{Atom.to_string(domain)}.\#{entity_id}"` in
`set_entity_state/2`, or `"services/\#{domain}/..."` in `set_service/2`).
* Incoming responses: `get_service_domains/0` reads each domain as a string from the decoded
JSON body (`item["domain"]`) and converts it back to an atom with `String.to_atom/1`
before returning it; `get_entities/0` does the same when splitting `"light.salon"` into
`{:light, "salon"}`.
So a domain only ever exists as a string right at the HTTP boundary; everywhere else in this
module's public API — every argument and every return value — it is an atom.
"""
# Needs to be fixed, go around the function and define types of entities and use them here
# @type entity_id :: String.t() | %{optional(String.t()) => String.t()}
# @type entity_type :: :num | :bool | :enum
@doc """
This function sends a POST request to the Home Assistant instance specified in the configuration.
* Input requires the path to the endpoint and a payload specifying the body to be sent.
* Returns a tuple {:ok, response} if the request is successful, where response is the decoded body of the response.
If an error occurs, the function returns {:error, error}, where the variable error contains the details of the error.
iex> {:ok, _} = HomeAssistantApiClient.entry_point_post("intent/handle", %{"name" => "HassGetState", "data" => %{"name" => "sensor.sensor_temperatura_mock"}})
"""
def entry_point_post(next, payload \\ "") do
url = Application.get_env(:home_assistant_api_client, :url)
token = Application.get_env(:home_assistant_api_client, :token)
headers = [
Authorization: "Bearer #{token}",
"Content-Type": "Application/json; Charset=utf-8"
]
Logger.debug(inspect(payload))
body = Poison.encode!(payload)
case HTTPoison.post("#{url}/api/#{next}", body, headers) do
{:ok, response} ->
Poison.decode(response.body)
{:error, error} ->
{:error, error}
end
end
@doc """
This function returns the response from GET requests pointing to the path ending with the input endpoint.
The response format is the same as for entry_point_post.
iex> {:ok, _} = HomeAssistantApiClient.entry_point_get("config")
"""
def entry_point_get(endpoint \\ "") do
url = Application.get_env(:home_assistant_api_client, :url)
token = Application.get_env(:home_assistant_api_client, :token)
headers = [Authorization: "Bearer #{token}", Accept: "Application/json; Charset=utf-8"]
case HTTPoison.get("#{url}/api/#{endpoint}", headers) do
{:ok, r} ->
r.body |> Poison.decode()
{:error, error} ->
{:error, error}
end
end
# ----------------------GET--------------
@doc """
Returns the complete configuration of the Home Assistant instance.
## Example
iex> {:ok, _} = HomeAssistantApiClient.get_config()
"""
@spec get_config() ::
{:ok, map()}
| {:error, any()}
def get_config do
entry_point_get("config")
end
@doc """
Returns the logbook entries, if a date is give, the format should be "YYYY-MM-DDThh:mm:ssTZD", if
no date is provided, the logs will be from the current day.
iex> HomeAssistantApiClient.get_logbook("")
iex> HomeAssistantApiClient.get_logbook("2025-11-03")
"""
@spec get_logbook() :: {atom(), list()}
@spec get_logbook(String.t()) :: {:ok, list()}
def get_logbook(timestamp \\ "") do
case timestamp do
"" ->
entry_point_get("logbook")
timestamp ->
entry_point_get("logbook/#{timestamp}")
end
end
@doc """
Returns the service domains in a map, and adds a key to each so that
they can be easily accessed later.
iex> {:ok, _element} = HomeAssistantApiClient.get_service_domains()
"""
@spec get_service_domains() :: {:ok, [atom()]} | {:error, String.t()}
def get_service_domains do
case entry_point_get("services") do
{:ok, res_with_all} when is_list(res_with_all) ->
{:ok,
res_with_all
|> Enum.with_index()
|> Enum.reduce([], fn {item, _idx}, acc ->
acc ++ [String.to_atom(item["domain"])]
end)}
_else ->
{:error, "There seems to be no entities"}
end
end
@doc """
Returns an specific domain with all his data.
iex> {:ok, [domain | _]} = HomeAssistantApiClient.get_service_domains()
iex> {:ok, element} = HomeAssistantApiClient.get_service_domain(domain)
iex> is_map(element)
"""
@spec get_service_domain(atom()) :: {atom(), map()}
def get_service_domain(domain) do
case entry_point_get("services") do
{:ok, res_with_all} when is_list(res_with_all) ->
find_service_domain(res_with_all, domain)
_ ->
Logger.warning("Error doing a get to the Home-Assistant API")
%{}
end
end
defp find_service_domain(res_with_all, domain) do
case Enum.find(res_with_all, fn elemnt ->
Map.get(elemnt, "domain") == Atom.to_string(domain)
end) do
nil -> {:error, "error: domain not found"}
elemnt -> {:ok, elemnt}
end
end
@doc """
Returns a map with all of its entity names
iex> {:ok, _names} = HomeAssistantApiClient.get_entities()
"""
@spec get_entities() :: {:ok, [{Types.entity_type(), Types.entity_id()}]} | {:error, String.t()}
def get_entities do
case entry_point_get("states") do
{:ok, res_with_all} when is_list(res_with_all) ->
entities =
Enum.map(res_with_all, fn item ->
[domain, name] = String.split(item["entity_id"], ".", parts: 2)
{String.to_atom(domain), name}
end)
{:ok, entities}
_else ->
{:error, "error: There seems to be no entities"}
end
end
@spec get_entity_types() :: {:ok, [Types.entity_type()]} | {:error, String.t()}
def get_entity_types do
case get_entities() do
{:ok, list} ->
types =
list
|> Enum.map(fn {type, _id} -> type end)
|> Enum.uniq()
{:ok, types}
{:error, error} ->
{:error, error}
end
end
@spec get_entity_types!() :: [Types.entity_type()] | no_return()
def get_entity_types! do
case get_entity_types() do
{:ok, valor} -> valor
{:error, error} -> raise(error)
end
end
@doc """
This function returns a list of {type, name} of entities of the type that you
specify.
iex> {:ok, _} = HomeAssistantApiClient.get_entities_by_type(:light)
"""
@spec get_entities_by_type(Types.entity_type()) ::
{:ok, [{Types.entity_type(), Types.entity_id()}]} | {:error, map()}
def get_entities_by_type(entity_type) do
{:ok, list} = get_entities()
# Filtramos por entity_type
filtered =
Enum.filter(list, fn {key, _valor} -> key == entity_type end)
# Retornamos en el formato correcto
{:ok, filtered}
end
@doc """
Returns the state for all of the entity types except for :input_select in which case it returns state and attributes.
iex> {:ok, [entity | _]} = HomeAssistantApiClient.get_entities()
iex> {:ok, _individual_entity} = HomeAssistantApiClient.get_entity(entity)
"""
@spec get_entity({:alarm_control_panel, Types.entity_id()}) ::
{:ok, Types.alarm_control_panel_state()} | {:error, String.t()}
@spec get_entity({:automation, Types.entity_id()}) ::
{:ok, Types.automation_state()} | {:error, String.t()}
@spec get_entity({:binary_sensor, Types.entity_id()}) ::
{:ok, Types.binary_sensor_state()} | {:error, String.t()}
@spec get_entity({:conversation, Types.entity_id()}) ::
{:ok, atom()} | {:error, String.t()}
@spec get_entity({:event, Types.entity_id()}) ::
{:ok, atom()} | {:error, String.t()}
@spec get_entity({:input_boolean, Types.entity_id()}) ::
{:ok, Types.input_boolean_state()} | {:error, String.t()}
@spec get_entity({:input_number, Types.entity_id()}) ::
{:ok, Types.input_number_state()} | {:error, String.t()}
@spec get_entity({:input_select, Types.entity_id()}) ::
{:ok, Types.input_select_state()} | {:error, String.t()}
@spec get_entity({:input_text, Types.entity_id()}) ::
{:ok, Types.input_text_state()} | {:error, String.t()}
@spec get_entity({:light, Types.entity_id()}) ::
{:ok, Types.light_state()} | {:error, String.t()}
@spec get_entity({:person, Types.entity_id()}) ::
{:ok, Types.person_state()} | {:error, String.t()}
@spec get_entity({:sensor, Types.entity_id()}) ::
{:ok, Types.sensor_state() | String.t()} | {:error, String.t()}
@spec get_entity({:sun, Types.entity_id()}) ::
{:ok, Types.sun_state()} | {:error, String.t()}
@spec get_entity({:switch, Types.entity_id()}) ::
{:ok, Types.switch_state()} | {:error, String.t()}
@spec get_entity({:timer, Types.entity_id()}) ::
{:ok, Types.switch_state()} | {:error, String.t()}
@spec get_entity({:todo, Types.entity_id()}) ::
{:ok, Types.todo_state()} | {:error, String.t()}
@spec get_entity({:tts, Types.entity_id()}) ::
{:ok, Types.tts_state()} | {:error, String.t()}
@spec get_entity({:weather, Types.entity_id()}) ::
{:ok, Types.weather()} | {:error, String.t()}
@spec get_entity({:zone, Types.entity_id()}) ::
{:ok, Types.zone_state()} | {:error, String.t()}
def get_entity({type, entity_id}) do
name = "#{Atom.to_string(type)}.#{entity_id}"
Logger.debug("get_entity request for #{name}")
case entry_point_get("states/#{name}") do
{:ok, res_with_all} when is_map(res_with_all) ->
Logger.debug(inspect(res_with_all))
if Map.has_key?(res_with_all, "state") do
{:ok, check_state_entrance(res_with_all["state"])}
else
{:error, "entity not found"}
end
_else ->
{:error, "code error"}
end
end
@doc """
Returns the state and available options of an `:input_select` entity.
iex> {:ok, %{state: _state, options: _options}} =
...> HomeAssistantApiClient.get_entity_attributes({:input_select, "puerta"})
"""
@spec get_entity_attributes({:input_select, Types.entity_id()}) ::
{:ok, %{state: Types.input_select_state(), options: [String.t()]}} | {:error, String.t()}
def get_entity_attributes({:input_select, entity_id}) do
name = "input_select.#{entity_id}"
case entry_point_get("states/#{name}") do
{:ok, res_with_all} when is_map(res_with_all) ->
if Map.has_key?(res_with_all, "state") do
{:ok,
%{
state: check_state_entrance(res_with_all["state"]),
options: res_with_all["attributes"]["options"]
}}
else
{:error, "entity not found"}
end
_else ->
{:error, "code error"}
end
end
# ------------POST--------------------------------------
# Documentar los tipos y el porque de cada tipo
defp check_send_boolean(true), do: "on"
defp check_send_boolean(false), do: "off"
@spec set_entity_state({:automation, Types.entity_id()}, Types.automation_state()) ::
{:ok, Types.automation_state()} | {:error, String.t()}
@spec set_entity_state({:input_boolean, Types.entity_id()}, Types.input_boolean_state()) ::
{:ok, Types.input_boolean_state()} | {:error, String.t()}
@spec set_entity_state({:input_number, Types.entity_id()}, Types.input_number_state()) ::
{:ok, Types.input_number_state()} | {:error, String.t()}
@spec set_entity_state({:input_text, Types.entity_id()}, Types.input_text_state()) ::
{:ok, Types.input_text_state()} | {:error, String.t()}
@spec set_entity_state({:input_select, Types.entity_id()}, Types.input_select_state()) ::
{:ok, Types.input_select_state()} | {:error, String.t()}
@spec set_entity_state({:light, Types.entity_id()}, Types.light_state()) ::
{:ok, Types.light_state()} | {:error, String.t()}
@spec set_entity_state({:switch, Types.entity_id()}, Types.switch_state()) ::
{:ok, Types.switch_state()} | {:error, String.t()}
@spec set_entity_state({:alarm_control_panel, Types.entity_id()}, Types.alarm_state()) ::
{:ok, Types.alarm_state()} | {:error, String.t()}
@doc """
For the function: set_entity_state({domain, entity_id}, state)
The input is the domain, using the type atom, the entity name specified as an String.t() and the state, note that
the state is specified in a type check which one to use for the entity type.
iex> {:ok, _element} = HomeAssistantApiClient.set_entity_state({:input_boolean, "luz_cuarto_gabriel_principal"}, false)
"""
# # input_select has its own function their types are specified for each entity
# def set_entity_state({:input_select, entity_id}, state) when is_binary(state) do
# Logger.info("set entity state")
# if check_states_for_input_select({:input_select, entity_id}, state) do
# # Ya se checkea que exista la entidad
# {:ok, _entity} = get_entity({:input_select, entity_id})
# Logger.info("get_entity working")
# modified_entity = Map.put(%{}, "state", state)
# Logger.info(modified_entity)
# send_to_instance_set_entity(:input_select, entity_id, modified_entity)
# else
# raise("This option state does not exists")
# end
# end
def set_entity_state({domain, entity_id}, state)
when domain in [
:light,
:input_boolean,
:switch,
:automation,
:remote,
:alarm_control_panel
] do
# Change boolean state to state accepted by home assistant
state = check_send_boolean(state)
modified_entity = Map.put(%{}, "state", state)
send_to_instance_set_entity(domain, entity_id, modified_entity)
end
def set_entity_state({domain, entity_id}, state)
when domain in [:input_select, :input_number, :timer, :cover, :input_text] do
modified_entity = Map.put(%{}, "state", state)
send_to_instance_set_entity(domain, entity_id, modified_entity)
end
@spec set_entity_state!({:error, any()}, any()) :: no_return()
@spec set_entity_state!({:ok, {atom(), Types.entity_id()}}, any()) :: any() | no_return()
def set_entity_state!({:error, _}, _new_state), do: raise("incorrect input")
def set_entity_state!({:ok, {domain, entity_id}}, new_state) do
case set_entity_state({domain, entity_id}, new_state) do
{:ok, elem} -> elem
{:error, _error} -> raise("Error in set_entity_state!")
end
end
defp send_to_instance_set_entity(domain, entity_id, modified_entity) do
Logger.debug("Ready for post")
case entry_point_post(
"states/#{Atom.to_string(domain)}.#{entity_id}",
modified_entity
) do
{:ok, response} ->
{:ok, check_state_entrance(response["state"])}
{:error, _error} ->
{:error, "State update failed"}
end
end
@domain_service_templates_without_extra %{
light: %{"entity_id" => "light.default"},
switch: %{"entity_id" => "switch.default"},
input_boolean: %{"entity_id" => "input_boolean.default"},
input_number: %{"entity_id" => "input_number.default"},
input_select: %{"entity_id" => "input_select.default"},
automation: %{"entity_id" => "automation.default"},
scene: %{"entity_id" => "scene.default"},
script: %{"entity_id" => "script.default"},
homeassistant: %{}
}
@domain_service_templates_with_extra %{
weather: %{"entity_id" => "weather.forecast_home", "type" => "daily"},
mqtt: %{"topic" => "home/test", "payload" => "ON", "retain" => true},
input_text: %{"entity_id" => "input_text.default", "value" => 0},
input_number: %{"entity_id" => "input_number.default", "value" => 0},
input_select: %{"entity_id" => "input_select.default", "option" => "default"}
}
@doc """
Builds the base payload for a service call on a domain that does not require extra
arguments (e.g. `:light`, `:switch`, `:automation`), merging in any `overrides`.
iex> HomeAssistantApiClient.build_payload_services_without(:light, %{"entity_id" => "light.salon"})
%{"entity_id" => "light.salon"}
"""
@spec build_payload_services_without(atom(), map()) :: map()
def build_payload_services_without(domain, overrides \\ %{}) do
base = Map.get(@domain_service_templates_without_extra, domain, %{})
Map.merge(base, overrides)
end
@doc """
Builds the base payload for a service call on a domain that requires an extra
argument (e.g. `:weather`, `:mqtt`, `:input_text`), merging in any `overrides`.
iex> HomeAssistantApiClient.build_payload_services_with(:input_text, %{"value" => "hola"})
%{"entity_id" => "input_text.default", "value" => "hola"}
"""
@spec build_payload_services_with(atom(), map()) :: map()
def build_payload_services_with(domain, overrides \\ %{}) do
base = Map.get(@domain_service_templates_with_extra, domain, %{})
Map.merge(base, overrides)
end
@doc """
In this function the value of a given service can be changed.
The domain type is specified in the service what is being done,
and within the change the entity and the value of the change are specified.
Domain: It has to be atom.
Service: Atom also.
change: The change has to be a map so that you can write all the entity_id names and all the values, etc...
extra: some services require an extra like set_value where the value needs to be passed, so in this cases there is
an extra slot in the funtion forthis change, the user can specify the change itself.
Example of change:
iex> {:ok, _element} = HomeAssistantApiClient.set_service({:input_boolean, "aire_central"}, :turn_off)
iex> {:ok, _element} = HomeAssistantApiClient.set_service({:input_number, "temperatura_simulada"}, :set_value, 20)
"""
@spec set_service(
{:light, Types.entity_ids()},
Types.light_services()
) ::
{:ok, any()} | {:error, any()}
@spec set_service({:switch, Types.entity_ids()}, Types.switch_services()) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:input_select, Types.entity_ids()},
:select_option,
String.t()
) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:switch, Types.entity_ids()},
Types.switch_services()
) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:input_number, Types.entity_ids()},
:increment | :decrement | :reload
) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:weather, Types.entity_ids()},
Types.weather_services(),
String.t() | [String.t()]
) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:mqtt, Types.entity_ids()},
:publish,
String.t() | boolean() | number()
) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:input_number, Types.entity_ids()},
:set_value,
number()
) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:input_text, Types.entity_ids()},
:set_value,
String.t()
) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:input_select, Types.entity_ids()},
:set_options,
[String.t()] | String.t()
) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{Types.domain_with_extra_args(), String.t() | [String.t()]},
atom(),
String.t() | Integer | map() | number()
) ::
{:ok, [atom()] | atom() | [float()] | [String.t()]} | {:error, any()}
@spec set_service(
:homeassistant,
:restart | :stop
) ::
{:ok, any()} | {:error, any()}
def set_service(:homeassistant, service) do
entry_point_post(
"services/homeassistant/#{Atom.to_string(service)}",
%{}
)
|> handle_set_service_response()
end
@spec set_service(
{:alarm_control_panel, Types.entity_ids()},
Types.alarm_state()
) ::
{:ok, any()} | {:error, any()}
@spec set_service({:alarm_control_panel, Types.entity_ids()}, :alarm_trigger) ::
{:ok, any()} | {:error, any()}
@spec set_service(
{:alarm_control_panel, Types.entity_ids()},
Types.alarm_password_services(),
String.t()
) ::
{:ok, any()} | {:error, any()}
def set_service({:alarm_control_panel, entities}, :alarm_trigger) do
payload = entity_format_list_or_single(:alarm_control_panel, entities)
entry_point_post(
"services/alarm_control_panel/alarm_trigger",
payload
)
|> handle_set_service_response(payload)
end
def set_service({domain, entities}, service) do
change_map = entity_format_list_or_single(domain, entities, %{})
payload = build_payload_services_without(domain, change_map)
entry_point_post(
"services/#{domain}/#{Atom.to_string(service)}",
payload
)
|> handle_set_service_response(change_map)
end
@spec set_service(
{Types.domain_without_extra_values(), String.t() | [String.t()]},
Types.basic_service()
) ::
{:ok, [atom()] | atom() | String.t() | [float()] | [String.t()]} | {:error, any()}
def set_service({:alarm_control_panel, entities}, service, code)
when service in [
:alarm_disarm,
:alarm_arm_home,
:alarm_arm_away,
:alarm_arm_night,
:alarm_arm_custom_bypass
] do
payload =
entity_format_list_or_single(:alarm_control_panel, entities, code)
entry_point_post(
"services/alarm_control_panel/#{Atom.to_string(service)}",
payload
)
|> handle_set_service_response(payload)
end
@spec set_service({:light, Types.entity_ids()}, atom(), integer()) ::
{:ok, any()} | {:error, any()}
@spec set_service({:tts, Types.entity_ids()}, atom(), String.t()) ::
{:ok, any()} | {:error, any()}
@spec set_service({:notify, Types.entity_ids()}, atom(), map()) ::
{:ok, any()} | {:error, any()}
def set_service({domain, entities}, service, extra) do
change_map = entity_format_list_or_single(domain, entities, extra)
payload = build_payload_services_with(domain, change_map)
entry_point_post(
"services/#{domain}/#{Atom.to_string(service)}",
payload
)
|> handle_set_service_response(change_map)
end
defp handle_set_service_response({:ok, []}) do
{:ok, []}
end
defp handle_set_service_response({:ok, list}, change_map) when is_list(list) do
matched_list =
Enum.filter(list, fn elem ->
compare_entities_regardless_of_type(
elem["entity_id"],
change_map["entity_id"]
)
end)
case matched_list do
[] ->
{:ok, []}
[single] ->
{:ok, check_state_entrance(single["state"])}
_ ->
states =
Enum.map(matched_list, fn element ->
check_state_entrance(element["state"])
end)
{:ok, states}
end
end
defp handle_set_service_response(other, _change_map) do
other
end
defp entity_format_list_or_single(domain, entities, extra \\ nil) do
base =
case entities do
list when is_list(list) ->
%{
"entity_id" =>
Enum.map(list, fn entity ->
"#{Atom.to_string(domain)}.#{entity}"
end)
}
entity ->
%{
"entity_id" => "#{Atom.to_string(domain)}.#{entity}"
}
end
template =
case extra do
nil ->
Map.get(@domain_service_templates_without_extra, domain, %{})
_ ->
Map.get(@domain_service_templates_with_extra, domain, %{})
end
computed_extra =
build_extra_by_domain(domain, extra)
template
|> Map.merge(base)
|> Map.merge(computed_extra)
end
defp build_extra_by_domain(:light, value) when is_integer(value) do
%{"brightness" => value}
end
defp build_extra_by_domain(:alarm_control_panel, code)
when is_binary(code) and code != "" do
%{"code" => code}
end
defp build_extra_by_domain(:input_number, value) when is_number(value) do
%{"value" => value}
end
defp build_extra_by_domain(:input_text, value) when is_binary(value) do
%{"value" => value}
end
defp build_extra_by_domain(:input_select, value) when is_binary(value) do
%{"option" => value}
end
defp build_extra_by_domain(:tts, message) when is_binary(message) do
%{"message" => message}
end
defp build_extra_by_domain(:notify, %{message: msg, title: title}) do
%{"message" => msg, "title" => title}
end
defp build_extra_by_domain(_domain, extra) when is_map(extra), do: extra
defp build_extra_by_domain(_domain, _), do: %{}
defp check_state_entrance(state) do
cond do
state == "on" ->
true
state == "off" ->
false
is_integer(state) ->
state
is_float(state) ->
state
is_binary(state) ->
check_binary_state_entrance(state)
true ->
state
end
end
defp check_binary_state_entrance(state) do
cond do
match?({_, ""}, Integer.parse(state)) ->
{int, ""} = Integer.parse(state)
int
match?({_, ""}, Float.parse(state)) ->
{float, ""} = Float.parse(state)
float
match?({:ok, _, _}, DateTime.from_iso8601(state)) ->
state
true ->
String.to_atom(state)
end
end
defp compare_entities_regardless_of_type(elem, change) do
if is_list(change) do
elem in change
else
elem == change
end
end
@doc """
Renders a Home Assistant [template](https://www.home-assistant.io/docs/configuration/templating/)
against the current state of the instance.
iex> {:ok, _value} = HomeAssistantApiClient.template_host("{{ states('sensor.sensor_temperatura_mock') }}")
"""
@spec template_host(String.t()) :: {atom(), String.t() | number() | list() | map()}
def template_host(body) do
entry_point_post("template", %{"template" => body})
end
@doc """
Triggers a check to the configuration.yaml file of home assistant returning a map with the errors, warnings that
the system could have.
Also it returns the overall status of the instance.
"""
@spec check_config() :: {:ok, map()} | {:error, any()}
def check_config do
entry_point_post("config/core/check_config", %{})
end
@doc """
Triggers a semantic intent in Home Assistant, simulating a voice query or virtual assistant action.
Allows executing intents like HassTurnOn, HassGetState, etc., with the necessary data so Home
Assistant interprets and responds as if it were a conversational interaction. The `intent`
component needs to be enabled in the instance's `configuration.yaml` file.
iex> {:ok, _map} = HomeAssistantApiClient.intent("HassGetState", %{"name" => "sensor.sensor_temperatura_mock"})
"""
@spec intent(String.t(), map()) :: {:ok, map()} | {:error, any()}
def intent(name, data \\ %{}) do
entry_point_post("intent/handle", %{"name" => name, "data" => data})
end
# ------------------DELETE---------------
@doc """
It delets the entity with the name provided as the input.
"""
@spec drop_entity({Types.entity_type(), Types.entity_id()}) :: {:ok, any()} | {:error, any()}
def drop_entity({domain, name}) do
url = Application.get_env(:home_assistant_api_client, :url)
token = Application.get_env(:home_assistant_api_client, :token)
headers = [
Authorization: "Bearer #{token}",
"Content-Type": "Application/json; Charset=utf-8"
]
case HTTPoison.delete("#{url}/api/states/#{Atom.to_string(domain)}.#{name}", headers) do
{:ok, response} -> Poison.decode(response.body)
{:error, error} -> {:error, error}
end
end
end
# Pipe commands: