Skip to content

Custom Home Assistant Integration

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

  1. Sign up at 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:

    Terminal window
    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.

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.

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

Response shape:

{
"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 Supabase user UUID; you don’t need to read it for the daily loop, but it’s useful 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 for the full lifecycle.

Step 2 — Wire credentials into Home Assistant secrets

Section titled “Step 2 — Wire credentials into Home Assistant secrets”

Add to secrets.yaml:

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

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

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 for the full request schema (humidity, target temp, fan mode, power draw all optional).

Step 4 — Pull the optimized schedule once a day

Section titled “Step 4 — Pull the optimized schedule once a day”

Set up a REST sensor that pulls today’s schedule after the nightly optimization runs:

rest:
- resource: "https://api.hungrymachines.io/api/v1/schedule"
headers:
Authorization: "Bearer !secret hm_access_token"
scan_interval: 86400 # once per day
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 for the full response shape.

Step 5 — Apply setpoints based on the current interval

Section titled “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 (the defaults path before the first thermal model is fit):

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 is not none %}
{{ 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):

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.

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

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.

Add an automation that refreshes the access token every 23 hours and writes it back to secrets.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.

DirectionEndpointFrequencyPurpose
PushPOST /api/v1/readingsEvery 5 minSensor data → optimizer trains thermal model
PushPOST /api/v1/appliances/{id}/readingsEvery 5 min (per appliance)EV / battery / water heater state
PullGET /api/v1/scheduleDaily after 05:00 UTCHVAC setpoints for next 24h
PullGET /api/v1/schedulesDaily after 05:00 UTCAll-appliance schedules in one call
CRUDPOST/GET/PUT /api/v1/appliancesOne-timeRegister devices, set constraints
CRUDGET/PUT /api/v1/preferencesAs-neededComfort settings

Full per-endpoint schemas are in the API Reference.

Your access token expired. Refresh it and retry — see Step 1 of Authentication.

The optimizer needs ~3 days of continuous readings before the fitter builds your first per-user thermal model. Until then, source: "defaults" returns a flat comfort band based on your preferences with setpoint_temps: null. Keep pushing readings — the daily 06:30 UTC initial-fit job picks new users up automatically once the data threshold is met.

Setpoints aren’t being applied to the thermostat

Section titled “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.