Hassock.Protocol (hassock v0.2.0)

Copy Markdown View Source

Pure functions for parsing and encoding Home Assistant WebSocket frames.

No side effects, no processes — data in, data out. Hassock.Connection uses these to translate between JSON on the wire and tagged Elixir terms.

HA WebSocket protocol summary

All messages are JSON objects with a type field. After authentication, client commands carry an integer id for response correlation.

Server → Client

  • auth_required, auth_ok, auth_invalid
  • result — response to a command (keyed by id)
  • event — subscription event (state_changed, entities deltas, etc.)
  • pong — response to ping

Client → Server

  • auth — send access token
  • subscribe_events — subscribe to a generic event type
  • subscribe_entities — efficient targeted entity subscription
  • unsubscribe_events — cancel any subscription
  • call_service, get_states, get_services, ping

Summary

Functions

Encode an authentication message.

Encode a call_service command.

Encode a get_services command.

Encode a get_states command.

Encode a ping command.

Encode a subscribe_entities command. Pass nil to subscribe to every entity, or a list of entity_ids to scope the subscription.

Encode a subscribe_events command for a given event type.

Encode an unsubscribe_events command (works for both subscription types).

Parse a raw JSON string from the HA WebSocket into a tagged term.

True if a new state differs from the prior in a meaningful way (state value or attributes). Returns true when old is nil. last_updated-only churn returns false.

Types

entities_payload()

@type entities_payload() :: %{
  added: %{required(String.t()) => Hassock.EntityState.t()},
  changed: %{required(String.t()) => entity_diff()},
  removed: [String.t()]
}

entity_diff()

@type entity_diff() :: %{
  added: %{
    optional(:state) => String.t(),
    optional(:attributes) => map(),
    optional(:last_changed) => String.t(),
    optional(:last_updated) => String.t()
  },
  removed: %{optional(:attributes) => [String.t()]}
}

parsed()

@type parsed() ::
  :auth_required
  | :auth_ok
  | {:auth_invalid, String.t()}
  | {:result, integer(), boolean(), term()}
  | {:state_changed, integer(), Hassock.EntityState.t(),
     Hassock.EntityState.t() | nil}
  | {:entities, integer(), entities_payload()}
  | {:event, integer(), String.t(), map()}
  | {:states, integer(), [Hassock.EntityState.t()]}
  | {:services, integer(), %{required(String.t()) => [String.t()]}}
  | :pong
  | {:unknown, map()}

Functions

encode_auth(token)

@spec encode_auth(String.t()) :: String.t()

Encode an authentication message.

encode_call_service(id, call)

@spec encode_call_service(integer(), Hassock.ServiceCall.t()) :: String.t()

Encode a call_service command.

encode_get_services(id)

@spec encode_get_services(integer()) :: String.t()

Encode a get_services command.

encode_get_states(id)

@spec encode_get_states(integer()) :: String.t()

Encode a get_states command.

encode_ping(id)

@spec encode_ping(integer()) :: String.t()

Encode a ping command.

encode_subscribe_entities(id, entity_ids)

@spec encode_subscribe_entities(integer(), [String.t()] | nil) :: String.t()

Encode a subscribe_entities command. Pass nil to subscribe to every entity, or a list of entity_ids to scope the subscription.

encode_subscribe_events(id, event_type)

@spec encode_subscribe_events(integer(), String.t()) :: String.t()

Encode a subscribe_events command for a given event type.

encode_unsubscribe_events(id, subscription_id)

@spec encode_unsubscribe_events(integer(), integer()) :: String.t()

Encode an unsubscribe_events command (works for both subscription types).

parse(json)

@spec parse(String.t()) :: parsed()

Parse a raw JSON string from the HA WebSocket into a tagged term.

state_changed?(old, new)

@spec state_changed?(Hassock.EntityState.t() | nil, Hassock.EntityState.t()) ::
  boolean()

True if a new state differs from the prior in a meaningful way (state value or attributes). Returns true when old is nil. last_updated-only churn returns false.