Hassock.Connection (hassock v0.3.0)

Copy Markdown View Source

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:

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.

Summary

Functions

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).

Returns a specification to start this module under a supervisor.

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

True if authenticated and the underlying socket is connected.

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

Fetch all available service domains and their services.

Fetch a one-shot snapshot of all entity states.

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

Connection status

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

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.

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

Stop sending lifecycle messages to the caller.

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.

Types

command_error()

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

status()

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

Functions

call_service(conn, call, opts \\ [])

@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(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

command(conn, type, payload \\ %{}, opts \\ [])

@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?(conn)

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

True if authenticated and the underlying socket is connected.

controlling_process(conn, new_pid)

@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(conn)

@spec get_services(GenServer.server()) ::
  {:ok, %{required(String.t()) => [String.t()]}} | {:error, command_error()}

Fetch all available service domains and their services.

get_states(conn)

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

Fetch a one-shot snapshot of all entity states.

start_link(opts)

@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(conn)

@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(conn, entity_ids \\ nil, opts \\ [])

@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_ids 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(conn, event_type, opts \\ [])

@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(conn, subscription_id)

@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(conn)

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

Stop sending lifecycle messages to the caller.

watch(conn)

@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.