# Custom Home Assistant Integration

Extend or replace the default Energy Dashboard with your own Home Assistant REST sensors, rest_command actions, and Lovelace cards backed by the Hungry Machines API.

Already a Home Assistant power user? You can build your own integration on top of the Hungry Machines API — custom Lovelace cards, automations that consume the schedule, dashboards tuned to your home. The default [Energy Dashboard](/home-assistant/) is open source and a good starting point if you want to fork it; this guide walks you through doing it from scratch.

You'll get the most flexibility out of this path, but you're responsible for token refresh, sensor wiring, and applying setpoints. The default dashboard handles those for you — only take this path if you want that control.

## Before you start
1. **Sign up at [hungrymachines.io/signup](https://hungrymachines.io/signup)** and confirm your email. Always sign up through the website — it sets up your billing, profile, and subscription state. Programmatic signup skips those steps.

2. **Verify Home Assistant can reach the API:**
   ```bash
   curl -sI https://api.hungrymachines.io/health
   ```
   You should see `HTTP/2 200`. If your HA instance is on a corporate network or VPN, make sure outbound HTTPS to `api.hungrymachines.io` is allowed.
## Step 1 — Get an access token

You'll exchange your Hungry Machines credentials for an access token by calling the API directly. Save the result somewhere durable — you'll wire it into Home Assistant secrets next.

```bash
curl -X POST https://api.hungrymachines.io/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your-password"}'
```

Response shape:

```json
{
  "access_token": "eyJhbGciOiJFUzI1NiIs...",
  "refresh_token": "v1.Mfg2...",
  "expires_in": 3600,
  "expires_at": 1735689600,
  "token_type": "bearer",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "you@example.com"
  }
}
```

The `user.id` is your Hungry Machines account id; you don't need it for the daily loop, but it's handy when correlating logs.

The access token is good for **1 hour**. The refresh token is good for **30 days**. Both rotate on every refresh — see the [Authentication reference](/getting-started/authentication/) for the full lifecycle.

## Step 2 — Wire credentials into Home Assistant secrets

Add to `secrets.yaml`:

```yaml
hm_access_token: "eyJhbGciOiJFUzI1NiIs..."
hm_refresh_token: "v1.Mfg2..."
```

> **Caution**
>
> Access tokens expire after an hour. For a long-running deployment, set up an automation that calls `POST /auth/refresh` daily and rewrites `hm_access_token` in `secrets.yaml`. There's a recipe at the bottom of this page.

## Step 3 — Push readings every 5 minutes

Define a `rest_command` in `configuration.yaml` that fires every 5 minutes:

```yaml
rest_command:
  hm_push_reading:
    url: "https://api.hungrymachines.io/api/v1/readings"
    method: POST
    headers:
      Authorization: "Bearer !secret hm_access_token"
      Content-Type: "application/json"
    payload: >
      {"readings": [{
        "timestamp": "{{ now().isoformat() }}",
        "indoor_temp": {{ states('sensor.indoor_temperature') }},
        "hvac_state": "{{ states('climate.thermostat') | upper }}",
        "outdoor_temp": {{ states('sensor.outdoor_temperature') }}
      }]}

automation:
  - alias: "HM — push reading every 5 min"
    trigger:
      - platform: time_pattern
        minutes: "/5"
    action:
      - service: rest_command.hm_push_reading
```

Adjust the entity IDs (`sensor.indoor_temperature`, `climate.thermostat`, `sensor.outdoor_temperature`) to match your setup.

See [API Reference — Readings](/api-reference/readings/) for the full request schema (humidity, target temp, fan mode, power draw all optional).

## Step 4 — Pull the schedule

Set up a REST sensor that pulls today's schedule. A plan for today can be replaced during the day, after a settings change and when the API re-plans the rest of the day, so refresh it every 15 minutes rather than once a day:

```yaml
rest:
  - resource: "https://api.hungrymachines.io/api/v1/schedule"
    headers:
      Authorization: "Bearer !secret hm_access_token"
    scan_interval: 900  # every 15 minutes, so a daytime re-plan is picked up
    sensor:
      - name: "HM Schedule Source"
        value_template: "{{ value_json.source }}"
      - name: "HM Savings Pct"
        value_template: "{{ value_json.estimated_savings_pct }}"
        unit_of_measurement: "%"
      - name: "HM Schedule Date"
        value_template: "{{ value_json.date }}"
```

The full schedule object (48 half-hour intervals with `high_temps` and `low_temps`) is exposed under `state_attr('sensor.hm_schedule_source', 'all_attributes')` if you parse the JSON yourself, or you can split each field into a separate sensor entry.

See [API Reference — Schedule](/api-reference/schedule/) for the full response shape.

## Step 5 — Apply setpoints based on the current interval

Each schedule has 48 half-hour intervals (`interval = hour * 2 + (minute >= 30)`). The thermostat command for each interval lives in `schedule.setpoint_temps` — apply that single value, not the high/low band. The high/low arrays are display-only comfort bounds for charting.

Build a template sensor that picks the current interval's target. Fall back to the midpoint of `high_temps`/`low_temps` when `setpoint_temps` is `null`, which is what `GET /api/v1/schedule` returns while `source` is `"defaults"` (no plan stored for the unit yet):

```yaml
template:
  - sensor:
      - name: "HM Current Setpoint"
        unit_of_measurement: "°F"
        state: >
          {% set s = state_attr('sensor.hm_schedule_full', 'schedule') %}
          {% set idx = (now().hour * 2) + (1 if now().minute >= 30 else 0) %}
          {% if s.setpoint_temps %}
            {{ s.setpoint_temps[idx] }}
          {% else %}
            {{ (s.high_temps[idx] + s.low_temps[idx]) / 2 }}
          {% endif %}
```

Then a simple automation applies the setpoint to your thermostat whenever it changes (the 30-min interval boundary will trigger a state change):

```yaml
automation:
  - alias: "HM — apply current setpoint"
    trigger:
      - platform: state
        entity_id: sensor.hm_current_setpoint
    action:
      - service: climate.set_temperature
        target:
          entity_id: climate.thermostat
        data:
          temperature: "{{ states('sensor.hm_current_setpoint') | float }}"
```

The exact `climate.set_temperature` payload depends on your thermostat integration — some climate entities want `temperature`, others `target_temp_low`/`target_temp_high`, others `target_temp_high` only. Check Developer Tools → Services for your specific entity.

## Step 6 — Custom Lovelace card

Once your sensors are wired up, build a Lovelace card however you want. A minimal example:

```yaml
type: vertical-stack
cards:
  - type: markdown
    content: |
      ## Hungry Machines
      **Source:** {{ states('sensor.hm_schedule_source') }}
      **Savings:** {{ states('sensor.hm_savings_pct') }}%
  - type: gauge
    entity: sensor.hm_current_high_temp
    min: 60
    max: 90
    name: "Cool to"
  - type: gauge
    entity: sensor.hm_current_low_temp
    min: 60
    max: 90
    name: "Heat to"
```

Apex Charts, Mini Graph Card, or any other community Lovelace plugin can render the 48-interval schedule as a series.

## Daily token refresh recipe

Add an automation that refreshes the access token every 23 hours and writes it back to `secrets.yaml`:

```yaml
rest_command:
  hm_refresh_token:
    url: "https://api.hungrymachines.io/auth/refresh"
    method: POST
    headers:
      Content-Type: "application/json"
    payload: >
      {"refresh_token": "!secret hm_refresh_token"}

automation:
  - alias: "HM — refresh access token"
    trigger:
      - platform: time_pattern
        hours: "/23"
    action:
      - service: rest_command.hm_refresh_token
        response_variable: refresh
      - service: shell_command.update_hm_secrets
        data:
          access_token: "{{ refresh.content.access_token }}"
          refresh_token: "{{ refresh.content.refresh_token }}"
```

Implementing the `shell_command.update_hm_secrets` is left to you — typically a small `sed` script that rewrites `secrets.yaml`. Or move the credentials into an `input_text` helper instead of `secrets.yaml` so you can update them without filesystem writes.

> **Note**
>
> The default Energy Dashboard handles all of this internally. If the manual refresh wiring feels fragile, just install the dashboard and let it manage the lifecycle.

## What you're sending vs receiving

| Direction | Endpoint | Frequency | Purpose |
|---|---|---|---|
| Push | `POST /api/v1/readings` | Every 5 min | Sensor data → optimizer trains thermal model |
| Push | `POST /api/v1/appliances/{id}/readings` | Every 5 min (per appliance) | EV / battery / water heater / solar state |
| Pull | `GET /api/v1/schedule` | After the overnight run, and again when it changes | One HVAC unit's setpoints for today |
| Pull | `GET /api/v1/schedules` | After the overnight run, and again when it changes | All-appliance schedules in one call, including per-slot mode and fan schedules |
| Pull | `GET /api/v1/schedules/updated-at` | Every few minutes | One timestamp that changes whenever today's schedules are replaced |
| CRUD | `POST/GET/PUT /api/v1/appliances` | One-time | Register devices, set constraints |
| CRUD | `GET/PUT /api/v1/preferences` | As-needed | Comfort settings |

Full per-endpoint schemas are in the [API Reference](/api-reference/readings/).

## Troubleshooting

### Readings rejected with 401

Your access token expired. Refresh it and retry — see Step 1 of [Authentication](/getting-started/authentication/#refresh-an-expired-token).

### Schedule sensor returns empty

Until the first plan is stored for the unit, `GET /api/v1/schedule` returns `source: "defaults"`: a comfort band based on your preferences, with `setpoint_temps` set to `null`. The template in Step 5 handles that by applying the band's midpoint. The first plan is stored by the first recompute (a settings save, or `POST /api/v1/schedule/recompute`) or the next overnight run, and from then on `source` is `"optimization"`. If you have 2+ HVAC units, add `?appliance_id=…` to the resource URL; without it the route answers `400`.

### Setpoints aren't being applied to the thermostat

Check that your `climate.set_temperature` service call uses the right keys for your thermostat. The example in Step 5 sends a single `temperature` value (which matches what most modern integrations want, and what the optimizer's `setpoint_temps` is designed for); some older integrations expect `target_temp_high`/`target_temp_low` instead. Test the call manually from Developer Tools → Services first.

## Next steps

- [Authentication](/getting-started/authentication/) — token lifecycle reference
- [API Reference — Schedules](/api-reference/schedule/) — full schedule response shape
- [API Reference — Appliances](/api-reference/appliances/) — registering EV chargers, batteries, water heaters
- [Build Your Own Client](/guides/build-your-own-client/) — same API, non-HA platforms

Canonical page: https://docs.hungrymachines.io/guides/custom-home-assistant/
