Hassock.Protocol (hassock v0.3.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
  • any other command through encode_command/3

Results are returned undecoded ({:result, id, success, result}): only the sender knows which command an id belongs to, so Hassock.Connection decodes them by command kind with decode_states/1 and decode_services/1.

Summary

Types

Registry change action; unrecognised actions are kept as strings.

Which registry a *_registry_updated event is about.

Functions

The call_service fields (everything but id and type) for a call.

Decode a get_services result into %{domain => [service_name]}.

Decode a get_states result into %Hassock.EntityState{} structs.

Encode an authentication message.

Encode a call_service command.

Encode any command: payload merged with id and type. Top-level payload keys may be atoms or strings; id and type always win.

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.

The HA event types that parse/1 decodes into :registry_updated.

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() | nil,
     Hassock.EntityState.t() | nil}
  | {:entities, integer(), entities_payload()}
  | {:registry_updated, integer(), registry_kind(), registry_action(),
     String.t() | nil}
  | {:event, integer(), String.t() | nil, map()}
  | {:pong, integer() | nil}
  | {:unknown, term()}
  | {:invalid, Exception.t()}

registry_action()

@type registry_action() :: :create | :update | :remove | String.t()

Registry change action; unrecognised actions are kept as strings.

registry_kind()

@type registry_kind() :: :floor | :area | :device | :entity | :label

Which registry a *_registry_updated event is about.

Functions

call_service_payload(call)

@spec call_service_payload(Hassock.ServiceCall.t()) :: map()

The call_service fields (everything but id and type) for a call.

decode_services(result)

@spec decode_services(map()) :: %{required(String.t()) => [String.t()]}

Decode a get_services result into %{domain => [service_name]}.

decode_states(states)

@spec decode_states([map()]) :: [Hassock.EntityState.t()]

Decode a get_states result into %Hassock.EntityState{} structs.

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_command(id, type, payload \\ %{})

@spec encode_command(integer(), String.t(), map()) :: String.t()

Encode any command: payload merged with id and type. Top-level payload keys may be atoms or strings; id and type always win.

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.

Text that isn't valid JSON, or JSON whose fields don't have the types HA sends, returns {:invalid, exception} rather than raising: frames come from the network, and one bad frame shouldn't take the connection down. Fields of an entities event that have the wrong type are dropped.

registry_event_types()

@spec registry_event_types() :: [String.t()]

The HA event types that parse/1 decodes into :registry_updated.

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.