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

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

# `entities_payload`

```elixir
@type entities_payload() :: %{
  added: %{required(String.t()) =&gt; Hassock.EntityState.t()},
  changed: %{required(String.t()) =&gt; entity_diff()},
  removed: [String.t()]
}
```

# `entity_diff`

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

# `parsed`

```elixir
@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`

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

Registry change action; unrecognised actions are kept as strings.

# `registry_kind`

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

Which registry a `*_registry_updated` event is about.

# `call_service_payload`

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

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

# `decode_services`

```elixir
@spec decode_services(map()) :: %{required(String.t()) =&gt; [String.t()]}
```

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

# `decode_states`

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

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

# `encode_auth`

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

Encode an authentication message.

# `encode_call_service`

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

Encode a `call_service` command.

# `encode_command`

```elixir
@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`

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

Encode a `get_services` command.

# `encode_get_states`

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

Encode a `get_states` command.

# `encode_ping`

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

Encode a `ping` command.

# `encode_subscribe_entities`

```elixir
@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`

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

Encode a `subscribe_events` command for a given event type.

# `encode_unsubscribe_events`

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

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

# `parse`

```elixir
@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`

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

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

# `state_changed?`

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

---

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