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