trmnl 0.1.0

BYOS (Bring Your Own Server) framework for TRMNL e-ink displays
Documentation
# TRMNL + Home Assistant Integration Design

## Overview

Two-way integration between TRMNL e-ink displays and Home Assistant:

1. **TRMNL displays HA data** - Show sensor states, calendars, etc. on the e-ink display
2. **HA controls TRMNL** - Automations trigger display updates, control refresh rates

## Architecture

```
┌─────────────────┐         ┌─────────────────┐         ┌─────────────┐
│  Home Assistant │ ◄─────► │   trmnl-rs      │ ◄─────► │   TRMNL     │
│                 │  REST   │   BYOS Server   │  HTTP   │   Device    │
│  - Sensors      │         │                 │         │             │
│  - Automations  │         │  - HaClient     │         │  - Display  │
│  - Services     │         │  - Webhooks     │         │  - Battery  │
└─────────────────┘         └─────────────────┘         └─────────────┘
```

## Feature: `homeassistant`

```toml
[dependencies]
trmnl = { version = "0.1", features = ["axum", "render", "homeassistant"] }
```

### HaClient - Fetch HA States

```rust
use trmnl::homeassistant::{HaClient, HaState};

// Initialize with your HA instance
let ha = HaClient::new(
    "http://homeassistant.local:8123",
    "your-long-lived-access-token"
);

// Fetch single entity
let temp = ha.get_state("sensor.living_room_temperature").await?;
println!("Temperature: {}°F", temp.state);

// Fetch multiple entities at once
let states = ha.get_states(&[
    "sensor.living_room_temperature",
    "binary_sensor.front_door",
    "sensor.energy_today",
]).await?;

// Access attributes
let temp = &states["sensor.living_room_temperature"];
println!("Unit: {}", temp.attributes.get("unit_of_measurement").unwrap());
```

### HaState Structure

```rust
pub struct HaState {
    pub entity_id: String,
    pub state: String,
    pub attributes: HashMap<String, serde_json::Value>,
    pub last_changed: DateTime<Utc>,
    pub last_updated: DateTime<Utc>,
}

impl HaState {
    /// Parse state as f64 (for sensors)
    pub fn as_f64(&self) -> Option<f64>;

    /// Parse state as bool (for binary sensors)
    pub fn as_bool(&self) -> Option<bool>;

    /// Check if entity is "on", "home", "open", etc.
    pub fn is_on(&self) -> bool;

    /// Get friendly name from attributes
    pub fn friendly_name(&self) -> Option<&str>;
}
```

### Webhook Endpoint - HA Triggers TRMNL

Add a webhook endpoint that HA can call to trigger immediate refresh:

```rust
use trmnl::homeassistant::webhook_handler;

let app = Router::new()
    .route("/api/display", get(display))
    .route("/api/webhook/refresh", post(webhook_handler));  // HA calls this
```

The webhook can:
- Trigger immediate display regeneration
- Change refresh rate temporarily
- Display a specific message/alert

### Schedule from HA Input Helpers

Use HA `input_datetime` and `input_number` helpers to control TRMNL schedule:

```rust
// Fetch schedule parameters from HA
let sleep_start = ha.get_state("input_datetime.trmnl_sleep_start").await?;
let sleep_end = ha.get_state("input_datetime.trmnl_sleep_end").await?;
let refresh_rate = ha.get_state("input_number.trmnl_refresh_rate").await?;
```

## Home Assistant Configuration

### 1. Create Long-Lived Access Token

Settings → Security → Long-lived access tokens → Create Token

### 2. TRMNL as REST Command (for automations)

```yaml
# configuration.yaml
rest_command:
  trmnl_refresh:
    url: "https://yourserver.com/api/webhook/refresh"
    method: POST
    headers:
      Authorization: "Bearer {{ states('input_text.trmnl_token') }}"
    payload: '{"action": "refresh"}'

  trmnl_alert:
    url: "https://yourserver.com/api/webhook/alert"
    method: POST
    headers:
      Authorization: "Bearer {{ states('input_text.trmnl_token') }}"
    payload: '{"message": "{{ message }}"}'
```

### 3. Automations

```yaml
# Refresh display when someone arrives home
automation:
  - alias: "TRMNL - Refresh on arrival"
    trigger:
      - platform: state
        entity_id: person.john
        to: "home"
    action:
      - service: rest_command.trmnl_refresh

  - alias: "TRMNL - Alert on door open too long"
    trigger:
      - platform: state
        entity_id: binary_sensor.front_door
        to: "on"
        for: "00:05:00"
    action:
      - service: rest_command.trmnl_alert
        data:
          message: "Front door has been open for 5 minutes!"
```

### 4. Input Helpers for Schedule Control

```yaml
# configuration.yaml
input_datetime:
  trmnl_sleep_start:
    name: "TRMNL Sleep Start"
    has_time: true
    has_date: false
  trmnl_sleep_end:
    name: "TRMNL Sleep End"
    has_time: true
    has_date: false

input_number:
  trmnl_refresh_rate:
    name: "TRMNL Refresh Rate"
    min: 60
    max: 3600
    step: 60
    unit_of_measurement: "seconds"
```

### 5. TRMNL Device as HA Sensor (optional)

Expose TRMNL battery and status to HA via REST sensor:

```yaml
# configuration.yaml
sensor:
  - platform: rest
    name: "TRMNL Battery"
    resource: "https://yourserver.com/api/status"
    value_template: "{{ value_json.battery_percentage }}"
    unit_of_measurement: "%"
    device_class: battery
    scan_interval: 300

  - platform: rest
    name: "TRMNL Last Update"
    resource: "https://yourserver.com/api/status"
    value_template: "{{ value_json.last_update }}"
    device_class: timestamp
```

## Example: HA Dashboard on TRMNL

```rust
use axum::{routing::get, Json, Router};
use trmnl::{DeviceInfo, DisplayResponse};
use trmnl::homeassistant::HaClient;
use trmnl::render::{render_html_to_png, RenderConfig};

async fn display(device: DeviceInfo) -> Json<DisplayResponse> {
    let ha = HaClient::from_env(); // Uses HA_URL and HA_TOKEN env vars

    // Fetch states
    let states = ha.get_states(&[
        "sensor.living_room_temperature",
        "sensor.outside_temperature",
        "binary_sensor.front_door",
        "binary_sensor.garage_door",
        "sensor.energy_today",
        "weather.home",
    ]).await.unwrap_or_default();

    // Build HTML
    let html = format!(r#"
        <html>
        <body style="width:800px; height:480px; background:white; padding:20px; font-family:sans-serif;">
            <h1>Home Status</h1>
            <div style="display:flex; gap:40px;">
                <div>
                    <h2>Climate</h2>
                    <p>Inside: {inside_temp}°F</p>
                    <p>Outside: {outside_temp}°F</p>
                    <p>Weather: {weather}</p>
                </div>
                <div>
                    <h2>Security</h2>
                    <p>Front Door: {front_door}</p>
                    <p>Garage: {garage}</p>
                </div>
                <div>
                    <h2>Energy</h2>
                    <p>Today: {energy} kWh</p>
                </div>
            </div>
        </body>
        </html>
    "#,
        inside_temp = states.get("sensor.living_room_temperature").map(|s| &s.state).unwrap_or(&"--".into()),
        outside_temp = states.get("sensor.outside_temperature").map(|s| &s.state).unwrap_or(&"--".into()),
        weather = states.get("weather.home").map(|s| &s.state).unwrap_or(&"--".into()),
        front_door = if states.get("binary_sensor.front_door").map(|s| s.is_on()).unwrap_or(false) { "Open" } else { "Closed" },
        garage = if states.get("binary_sensor.garage_door").map(|s| s.is_on()).unwrap_or(false) { "Open" } else { "Closed" },
        energy = states.get("sensor.energy_today").map(|s| &s.state).unwrap_or(&"--".into()),
    );

    // Render and serve
    let png = render_html_to_png(&html, &RenderConfig::default()).await.unwrap();
    let filename = format!("{}.png", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
    std::fs::write(format!("/var/www/trmnl/{}", filename), &png).unwrap();

    Json(DisplayResponse::new(
        format!("https://myserver.com/trmnl/{}", filename),
        filename,
    ))
}
```

## API Endpoints

### GET /api/status

Returns TRMNL device status (for HA sensors):

```json
{
  "battery_percentage": 85,
  "battery_voltage": 3.95,
  "last_update": "2024-01-15T10:30:00Z",
  "refresh_rate": 300,
  "firmware_version": "1.2.3",
  "wifi_rssi": -45
}
```

### POST /api/webhook/refresh

Triggers immediate display refresh:

```json
{
  "action": "refresh"
}
```

### POST /api/webhook/alert

Displays an alert message:

```json
{
  "message": "Front door open!",
  "duration": 300  // seconds to show alert
}
```

### POST /api/webhook/config

Updates configuration:

```json
{
  "refresh_rate": 60,
  "schedule": "active"  // or "sleep"
}
```

## Implementation Plan

### Phase 1: HaClient (read HA states)
- [ ] `HaClient::new(url, token)`
- [ ] `HaClient::from_env()`
- [ ] `get_state(entity_id)`
- [ ] `get_states(&[entity_ids])`
- [ ] `HaState` struct with helpers

### Phase 2: Webhooks (HA triggers TRMNL)
- [ ] `/api/webhook/refresh` endpoint
- [ ] `/api/webhook/alert` endpoint
- [ ] Token authentication for webhooks

### Phase 3: Status API (TRMNL as HA sensor)
- [ ] `/api/status` endpoint
- [ ] Track last device contact
- [ ] Store battery/signal info

### Phase 4: Documentation
- [ ] HA configuration examples
- [ ] Automation recipes
- [ ] Dashboard template