# ADR-0013: Session-Owned Protocol Liveness (E27 `r_u_alive` Keepalive)

Date: 2025-12-27  
Status: Accepted

## Context

The Elk E27 Dealer API and observed panel behavior indicate that a long-lived TCP connection may be closed by the panel or network if the client remains idle. In live testing, an otherwise healthy connection ran for a period and then failed with `Connection reset by peer` when no traffic was sent for an interval.

The Dealer API documents a liveness request/response (`system.r_u_alive`) intended to indicate that the control is still present. The E27 comms guidance describes maintaining a 24/7 TCP connection and reconnecting with a new HELLO when the socket is closed.

We must decide where protocol liveness (keepalive) belongs in the architecture, consistent with DDR-0034:

- `Session` is responsible for transport lifecycle, HELLO, framing receive pump, disconnect/reconnect mechanics, and message dispatch.
- Linking and authentication are explicit operations owned above `Session` and are out of scope for `Session`.
- Feature modules represent panel capabilities (domain semantics), update state, and emit events.
- Home Assistant and sample applications should not be required to implement protocol plumbing.

## Decision

Protocol liveness for E27 SHALL be owned by `Session`.

`Session` SHALL implement an internal keepalive loop that periodically sends `system.r_u_alive` while the session is ACTIVE, correlates the response by sequence number, and uses timeouts/failures as a connection-health signal to drive reconnect behavior.

Keepalive messages and responses SHALL be handled privately by `Session` and SHALL NOT be exposed through feature modules, application-level request registries, or Home Assistant integration surfaces.

## Rationale

### Keepalive is transport health, not a panel capability

`system.r_u_alive` exists to prevent idle TCP teardown and to detect dead peers. This is a property of maintaining the transport session and does not represent an application feature or panel capability that end users should choose to invoke.

### Keepalive must run even when the application is idle

Relying on normal application traffic (polling, user actions) to keep the connection alive is insufficient in an event-driven architecture and would require Home Assistant or sample apps to implement protocol scheduling logic.

### Keepalive failure semantics differ from normal API call failures

When keepalive times out or sending fails, the correct action is to treat the connection as unhealthy and reconnect, rather than reporting a domain-level API failure to the caller. This belongs to `Session`’s transport lifecycle responsibility.

### Avoid leaking protocol plumbing to higher layers

Implementing keepalive as a feature request/handler would:
- expose keepalive to the outward API surface,
- require HA/sample programs to schedule it,
- create unnecessary state/events/noise,
- blur the separation between transport mechanics and feature semantics.

### Reuse mechanics, not abstraction

While keepalive uses the same wire format and may reuse request/response mechanics (seq allocation, pending correlation, timeout waiting), it remains a private `Session` responsibility and is not registered or dispatched as a feature.

## Consequences

### Positive

- Home Assistant integration and sample applications remain unaware of keepalive requirements.
- `Session.connect()` delivers a connection that remains stable during idle periods.
- Keepalive response timeouts become a reliable signal to trigger reconnect.
- Protocol liveness logic is testable at the `Session` layer and can be tuned via internal configuration.

### Negative / Tradeoffs

- `Session` becomes slightly more complex (a keepalive loop, pending correlation, and timeout handling).
- Care must be taken to ensure keepalive responses are swallowed internally and do not leak into application dispatch.
- Incorrect keepalive intervals or timeouts could cause unnecessary reconnect churn; defaults must be conservative.

## Implementation Notes

- Keepalive runs only while `SessionState.ACTIVE`.
- Each keepalive uses a distinct `seq` and is correlated by `seq` (Option B).
- `Session` logs a warning when a keepalive response is not received within `keepalive_timeout_s`.
- `Session` triggers reconnect after `keepalive_max_missed` consecutive keepalive timeouts (default 2).
- Keepalive send failures (`ConnectionResetError`, `BrokenPipeError`, `OSError`) are treated as connection failure and drive the existing disconnect/reconnect path.
- Keepalive responses are consumed internally and not forwarded to `on_message` / dispatcher / features.

## Related Decisions

- DDR-0034: Session responsibility boundaries (transport lifecycle, HELLO, framing receive pump, reconnect; no linking/auth orchestration).
- ADR-0011 / ADR-010x: E27 framing and session mechanics (where applicable).
