defmodule FritzApi do
@moduledoc """
A Fritz!Box Home Automation API Client for Elixir.
## Usage
iex> {:ok, client} = FritzApi.Client.new()
...> |> FritzApi.Client.login("admin", "changeme")
iex> FritzApi.set_switch_off(client, "687690315761")
:ok
iex> FritzApi.get_temperature(client, "687690315761")
{:ok, 23.5}
## Configuration
The main way to configure FritzApi is through the options passed to `FritzApi.Client.new/1`.
To customize the behaviour of the HTTP client used by FritzApi, you need to configure FritzApi
through the application environment. Configure the following keys under the `:fritz_api`
application. For example, you can do this in `config/config.exs`:
# config/config.exs
config :fritz_api,
client: MyHTTPClient,
client_pool_opts: [],
client_request_opts: [receive_timeout: 15_000]
To customize the behaviour of the HTTP client used by FritzApi, you need to configure FritzApi
through the application environment.
You can use these options:
- `:client` (`t:module/0`) - A module that implements the `FritzApi.HTTPClient`
behaviour. Defaults to `FritzApi.HTTPClient.Finch` (requires `:finch`).
- `:client_pool_opts` (`t:keyword/0`) - Options to configure the HTTP client pool. See
`Finch.start_link/1`. Defaults to `[]`.
- `:client_request_opts` (`t:keyword/0`) - Options passed to the `c:FritzApi.HTTPClient.get/2`
callback. See `Finch.request/3`. Defaults to `[]`.
"""
alias FritzApi.{Client, Error, Actor}
@typedoc """
Name of the FritzBox user.
> #### Note {: .info}
>
> With FRITZ! OS 7.24 and later, the user name cannot be empty.
"""
@type useranme :: String.t()
@typedoc "Password of the FritzBox user."
@type password :: String.t()
@typedoc "Unique actor identifier."
@type ain :: String.t()
@doc """
Get essential information of all smart home devices.
## Example
iex> FritzApi.get_device_list_infos(client)
{:ok, [%FritzApi.Actor{
ain: "687690315761",
alert: nil,
functions: ["Energie Messgerät", "Temperatursensor",
"Schaltsteckdose", "Mikrofon"],
fwversion: "04.17",
id: "1",
manufacturer: "AVM",
name: "Aussensteckdose",
powermeter: %FritzApi.Powermeter{
energy: 8.94,
power: 0.0,
voltage: 231.17
},
present: true,
productname: "FRITZ!DECT 210",
switch: %FritzApi.Switch{
devicelock: false,
lock: false,
mode: :auto,
state: false
},
temperature: %FritzApi.Temperature{
celsius: 21.0,
offset: 0.0
}
}]}
"""
@spec get_device_list_infos(Client.t()) :: {:error, Error.t()} | {:ok, [Actor.t()]}
def get_device_list_infos(%Client{} = client) do
case Client.execute_command(client, "getdevicelistinfos") do
{:ok, %{"devicelist" => %{"#content" => %{"device" => devices}}}} when is_list(devices) ->
{:ok, Enum.map(devices, &Actor.into/1)}
{:ok, %{"devicelist" => %{"#content" => %{"device" => device}}}} when is_map(device) ->
{:ok, [Actor.into(device)]}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Get the actuator identification numbers (AIN) of all known actors.
## Example
iex> FritzApi.get_switch_list(client)
{:ok, ["687690315761"]}
"""
@spec get_switch_list(Client.t()) :: {:error, Error.t()} | {:ok, [ain]}
def get_switch_list(%Client{} = client) do
case Client.execute_command(client, "getswitchlist") do
{:ok, ains} when is_binary(ains) ->
{:ok, ains |> String.trim_trailing() |> String.split(",")}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Turn on the switch.
## Example
iex> FritzApi.set_switch_on(client, "687690315761")
:ok
"""
@spec set_switch_on(Client.t(), ain) :: {:error, Error.t()} | :ok
def set_switch_on(%Client{} = client, ain) do
case Client.execute_command(client, "setswitchon", ain: ain) do
{:ok, "1"} -> :ok
{:error, reason} -> {:error, reason}
end
end
@doc """
Turn off the switch.
## Example
iex> FritzApi.set_switch_off(client, "687690315761")
:ok
"""
@spec set_switch_off(Client.t(), ain) :: {:error, Error.t()} | :ok
def set_switch_off(%Client{} = client, ain) do
case Client.execute_command(client, "setswitchoff", ain: ain) do
{:ok, "0"} -> :ok
{:error, reason} -> {:error, reason}
end
end
@doc """
Toggle the switch.
## Example
iex> FritzApi.set_switch_toggle(client, "687690315761")
{:ok, :off}
"""
@spec set_switch_toggle(Client.t(), ain) :: {:error, Error.t()} | {:ok, :on | :off}
def set_switch_toggle(%Client{} = client, ain) do
case Client.execute_command(client, "setswitchtoggle", ain: ain) do
{:ok, "1"} -> {:ok, :on}
{:ok, "0"} -> {:ok, :off}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get the current switching state.
Returns `{:ok, :unknown}` if the state is unknown.
## Example
iex> FritzApi.get_switch_state(client, "687690315761")
{:ok, :on}
"""
@spec get_switch_state(Client.t(), ain) :: {:error, Error.t()} | {:ok, :unknown | :on | :off}
def get_switch_state(%Client{} = client, ain) do
case Client.execute_command(client, "getswitchstate", ain: ain) do
{:ok, "1"} -> {:ok, :on}
{:ok, "0"} -> {:ok, :off}
{:ok, "inval"} -> {:ok, :unknown}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get the current connection state of the actor.
## Example
iex> FritzApi.get_switch_present(client, "687690315761")
{:ok, true}
"""
@spec get_switch_present(Client.t(), ain) :: {:error, Error.t()} | {:ok, boolean}
def get_switch_present(%Client{} = client, ain) do
case Client.execute_command(client, "getswitchpresent", ain: ain) do
{:ok, "1"} -> {:ok, true}
{:ok, "0"} -> {:ok, false}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get the current power consumption (Watt) of the switch.
Returns `{:ok, :unknown}` if the state is unknown.
## Example
iex> FritzApi.get_switch_power(client, "687690315761")
{:ok, 0.0}
"""
@spec get_switch_power(Client.t(), ain) :: {:error, Error.t()} | {:ok, :unknown | float}
def get_switch_power(%Client{} = client, ain) do
case Client.execute_command(client, "getswitchpower", ain: ain) do
{:ok, "inval"} -> {:ok, :unknown}
{:ok, power} when is_binary(power) -> {:ok, to_float(power, 3)}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get the total energy usage (kWh) of the switch.
Returns `{:ok, :unknown}` if the state is unknown.
## Example
iex> FritzApi.get_switch_energy(client, "687690315761")
{:ok, 0.475}
"""
@spec get_switch_energy(Client.t(), ain) :: {:error, Error.t()} | {:ok, :unknown | float}
def get_switch_energy(%Client{} = client, ain) do
case Client.execute_command(client, "getswitchenergy", ain: ain) do
{:ok, "inval"} -> {:ok, :unknown}
{:ok, energy} when is_binary(energy) -> {:ok, to_float(energy, 3)}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get the name of the actor.
## Example
iex> FritzApi.get_switch_name(client, "687690315761")
{:ok, "FRITZ!DECT #1"}
"""
@spec get_switch_name(Client.t(), ain) :: {:error, Error.t()} | {:ok, String.t()}
def get_switch_name(%Client{} = client, ain) do
Client.execute_command(client, "getswitchname", ain: ain)
end
@doc """
Get the last measured temperature (Celsius) of the actor.
Returns `{:ok, :unknown}` if the temperature could not be measured.
## Example
iex> FritzApi.get_temperature(client, "687690315761")
{:ok, 23.5}
"""
@spec get_temperature(Client.t(), ain) :: {:error, Error.t()} | {:ok, :unknown | float}
def get_temperature(%Client{} = client, ain) do
case Client.execute_command(client, "gettemperature", ain: ain) do
{:ok, "inval"} -> {:ok, :unknown}
{:ok, temp} when is_binary(temp) -> {:ok, to_float(temp, 1)}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get the target temperature (Celsius) currently set for the radiator
controller.
## Example
iex> FritzApi.get_hkr_target_temperature(client, "687690315761")
{:ok, 23.5}
"""
@spec get_hkr_target_temperature(Client.t(), ain) ::
{:error, Error.t()} | {:ok, :unknown | :on | :off | float}
def get_hkr_target_temperature(%Client{} = client, ain) do
case Client.execute_command(client, "gethkrtsoll", ain: ain) do
{:ok, value} when is_binary(value) -> {:ok, from_hkr_temp(value)}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get the comfort temperature (Celsius) set for time switching of the radiator
controller.
## Example
iex> FritzApi.get_hkr_comfort_temperature(client, "687690315761")
{:ok, 23.5}
"""
@spec get_hkr_comfort_temperature(Client.t(), ain) ::
{:error, Error.t()} | {:ok, :unknown | :on | :off | float}
def get_hkr_comfort_temperature(%Client{} = client, ain) do
case Client.execute_command(client, "gethkrkomfort", ain: ain) do
{:ok, value} when is_binary(value) -> {:ok, from_hkr_temp(value)}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get the economy temperature (Celsius) set for time switching of the radiator
controller.
## Example
iex> FritzApi.get_hkr_economy_temperature(client, "687690315761")
{:ok, 23.5}
"""
@spec get_hkr_economy_temperature(Client.t(), ain) ::
{:error, Error.t()} | {:ok, :unknown | :on | :off | float}
def get_hkr_economy_temperature(%Client{} = client, ain) do
case Client.execute_command(client, "gethkrabsenk", ain: ain) do
{:ok, value} when is_binary(value) -> {:ok, from_hkr_temp(value)}
{:error, reason} -> {:error, reason}
end
end
@doc """
Set the target temperature (Celsius) of the radiator controller.
## Example
iex> FritzApi.set_hkr_target_temperature(client, "687690315761", 21.5)
{:ok, 23.5}
"""
@spec set_hkr_target_temperature(Client.t(), ain, 8..28) :: {:error, Error.t()} | :ok
def set_hkr_target_temperature(%Client{} = client, ain, temp)
when is_number(temp) and (temp >= 8.0 and temp <= 28.0) do
case Client.execute_command(client, "sethkrtsoll", ain: ain, param: to_hkr_temp(temp)) do
{:ok, _unknown_response} -> :ok
{:error, reason} -> {:error, reason}
end
end
@doc """
Enabele the target temperature of the radiator controller.
## Example
iex> FritzApi.enable_hkr_target_temperature(client, "687690315761")
{:ok, 23.5}
"""
@spec enable_hkr_target_temperature(Client.t(), ain) :: {:error, Error.t()} | :ok
def enable_hkr_target_temperature(%Client{} = client, ain) do
case Client.execute_command(client, "sethkrtsoll", ain: ain, param: 254) do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}
end
end
@doc """
Disable the target temperature of the radiator controller.
## Example
iex> FritzApi.disable_hkr_target_temperature(client, "687690315761")
{:ok, 23.5}
"""
@spec disable_hkr_target_temperature(Client.t(), ain) :: {:error, Error.t()} | :ok
def disable_hkr_target_temperature(%Client{} = client, ain) do
case Client.execute_command(client, "sethkrtsoll", ain: ain, param: 253) do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}
end
end
defp to_float(value, dec_places)
when is_binary(value) and is_number(dec_places) and dec_places > 0 do
{int, ""} = Integer.parse(value)
int / :math.pow(10, dec_places)
end
defp from_hkr_temp(value) when is_binary(value) do
case Integer.parse(value) do
{253, ""} -> :off
{254, ""} -> :on
{int, ""} when int in 16..56 -> int / 2
end
end
defp to_hkr_temp(temp) when is_number(temp) do
round(temp * 2) |> min(56) |> max(16)
end
end