# `Hassock.Connection`
[🔗](https://github.com/bbangert/hassock/blob/v0.3.0/lib/hassock/connection.ex#L1)

WebSocket client for Home Assistant.

`start_link/1` returns at once; the connection is made in the background
and re-made after every loss, with exponential backoff (see
`Hassock.Connection.Backoff`). Home Assistant being down at boot never
blocks the caller or its supervisor.

The process that calls `start_link/1` becomes the **controlling process**
and receives async messages of the shape `{:hassock, conn, payload}`:

  * `{:hassock, conn, :connected}` - after each successful `auth_ok`
  * `{:hassock, conn, {:event, payload}}` - for each inbound HA event
  * `{:hassock, conn, {:disconnected, reason}}` - once per outage, when the
    connection is lost (or the first attempt fails); retries are silent
  * `{:hassock, conn, {:auth_failed, reason}}` - on `auth_invalid`. The
    connection then stops retrying (HA bans IPs after repeated failed
    logins); restart it with a fixed token. A controller set with
    `controlling_process/2` after the failure gets this message then.

Other processes can receive the lifecycle messages (all of the above
except events) with `watch/1`.

If a subscription can't be restored after a reconnect (HA returned an
error), it is dropped and its receiver gets
`{:hassock, conn, {:subscription_lost, subscription_id, error}}`.

Event payload variants:

  * `{:state_changed, new_state, old_state}` - from `subscribe_events("state_changed")`;
    `new_state` is `nil` when the entity was removed
  * `{:entities, %{added: _, changed: _, removed: _}}` - from `subscribe_entities/2`
  * `{:registry_updated, kind, action, id}` - from `subscribe_events/2` with
    one of `Hassock.Protocol.registry_event_types/0`
  * `{:other, event_type, raw_event}` - from any other `subscribe_events/2`
    type, and (with `event_type` `nil`) events without one, such as those
    of a `render_template` subscription made with `command/4`

## Subscriptions

Subscriptions survive reconnects: after each re-auth the connection
re-sends every active subscription, and the id returned by
`subscribe_entities/2` / `subscribe_events/2` stays valid. A
`subscribe_entities/2` subscription starts with a full snapshot again.

Events go to the controlling process (and follow it through
`controlling_process/2`), unless the subscription is made with the `:to`
option: then they go to that pid, and the subscription is cancelled when
it exits. `Hassock.RegistryCache` uses this to run beside another
controller.

## Keepalive

While connected, the connection sends HA a `ping` every `:ping_interval` ms
(see `Hassock.Config`) and reconnects if the `pong` doesn't arrive within
`:ping_timeout`, so a half-open TCP link is noticed. An attempt that
hasn't authenticated within `:connect_timeout` is abandoned and retried.

# `command_error`

```elixir
@type command_error() ::
  map()
  | :not_connected
  | :disconnected
  | :timeout
  | {:connection_down, term()}
  | {:unexpected_result, term()}
```

# `status`

```elixir
@type status() :: :connecting | :authenticating | :connected | :waiting | :auth_failed
```

# `call_service`

```elixir
@spec call_service(GenServer.server(), Hassock.ServiceCall.t(), keyword()) ::
  {:ok, map()} | {:error, command_error()}
```

Call a Home Assistant service and wait for the result: the raw result map,
with the service's response data under `"response"` when the call sets
`return_response: true`. Options: `:timeout` in ms (default 10 s).

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `command`

```elixir
@spec command(GenServer.server(), String.t(), map(), keyword()) ::
  {:ok, term()} | {:error, command_error()}
```

Send any Home Assistant WebSocket command and wait for its result.

`payload` is merged into the message next to `id` and `type`:

    Hassock.command(conn, "config/area_registry/list")
    Hassock.command(conn, "config/entity_registry/get", %{entity_id: "light.kitchen"}, timeout: 5_000)

Returns `{:ok, result}`, `{:error, %{"code" => _, "message" => _}}` for an
HA error, or `{:error, :not_connected | :disconnected | :timeout}`.
Options: `:timeout` in ms (default 10 s).

Use it for one-shot commands. A subscription made through it is not
restored after a reconnect and its events go to the controlling process;
prefer `subscribe_events/2` and `subscribe_entities/2`.

# `connected?`

```elixir
@spec connected?(GenServer.server()) :: boolean()
```

True if authenticated and the underlying socket is connected.

# `controlling_process`

```elixir
@spec controlling_process(GenServer.server(), pid()) ::
  :ok
  | {:error, :not_owner | :not_alive | :timeout | {:connection_down, term()}}
```

Transfer the controlling process. Only the current controller may transfer
ownership, or anyone once the controller has exited.

# `get_services`

```elixir
@spec get_services(GenServer.server()) ::
  {:ok, %{required(String.t()) =&gt; [String.t()]}} | {:error, command_error()}
```

Fetch all available service domains and their services.

# `get_states`

```elixir
@spec get_states(GenServer.server()) ::
  {:ok, [Hassock.EntityState.t()]} | {:error, command_error()}
```

Fetch a one-shot snapshot of all entity states.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start a Home Assistant connection. Returns once the process is up; it
connects in the background.

Options:

  * `:config` - `%Hassock.Config{}` (required)
  * `:name` - register the process under this name (optional)
  * `:controlling_pid` - receiver of async messages (default: caller)

The controller receives `{:hassock, ^conn, :connected}` once
authentication completes, and again after every reconnect.

# `status`

```elixir
@spec status(GenServer.server()) ::
  %{
    status: status(),
    attempt: non_neg_integer(),
    next_retry_in: non_neg_integer() | nil
  }
  | {:error, term()}
```

Connection status:

  * `:status` - `:connecting`, `:authenticating`, `:connected`, `:waiting`
    (a retry is scheduled) or `:auth_failed`
  * `:attempt` - consecutive failed attempts; it resets once a connection
    has stayed up for `backoff_max`, so a server that accepts and then
    drops the connection is still backed off
  * `:next_retry_in` - ms until the next attempt while `:waiting`, else `nil`

# `subscribe_entities`

```elixir
@spec subscribe_entities(GenServer.server(), [String.t()] | nil, keyword()) ::
  {:ok, integer()} | {:error, command_error()}
```

Subscribe to entity state updates. Pass `nil` to subscribe to all entities,
or a list of `entity_id`s to scope. Strictly preferred over
`subscribe_events("state_changed")`.

Returns `{:ok, subscription_id}` on success. Options:

  * `:to` - deliver the events to this pid instead of the controlling
    process (see "Subscriptions" above)
  * `:timeout` - ms to wait for HA (default 10 s); a subscription whose
    result comes after the caller gave up is cancelled

# `subscribe_events`

```elixir
@spec subscribe_events(GenServer.server(), String.t(), keyword()) ::
  {:ok, integer()} | {:error, command_error()}
```

Subscribe to an HA event type (e.g. `"automation_triggered"`). Don't use
this with `"state_changed"` - use `subscribe_entities/2` instead.
Options: `:to` and `:timeout`, as for `subscribe_entities/3`.

# `unsubscribe_events`

```elixir
@spec unsubscribe_events(GenServer.server(), integer()) ::
  :ok | {:error, command_error()}
```

Cancel a subscription returned by `subscribe_entities/2` or
`subscribe_events/2`. While disconnected this just forgets the
subscription, so it isn't restored.

# `unwatch`

```elixir
@spec unwatch(GenServer.server()) ::
  :ok | {:error, :timeout | {:connection_down, term()}}
```

Stop sending lifecycle messages to the caller.

# `watch`

```elixir
@spec watch(GenServer.server()) ::
  {:ok, status()} | {:error, :timeout | {:connection_down, term()}}
```

Also send the lifecycle messages (`:connected`, `{:disconnected, _}`,
`{:auth_failed, _}`) to the caller, until it exits or calls `unwatch/1`.
Returns the current status, so the caller knows whether to wait for
`:connected`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
