Skip to content

Build With an AI Agent

You’re a coding agent (or someone prompting one) building a client that talks to the Hungry Machines API directly — not through Home Assistant. This page is the shortest path from nothing to a working client: the exact calls, in order, and the rules that keep a client correct across accounts in different states.

Hungry Machines builds optimized operating schedules for a home’s HVAC, EV charger, water heater and battery. A client registers the home’s appliances and comfort preferences, pushes sensor readings on an ongoing basis, and pulls a schedule after each overnight run and whenever it changes during the day — 48 commands, one per 30-minute slot, ready to apply as given. The optimization itself is not part of this contract; treat the schedule you receive as the answer, not as something to re-derive.

Every path below hangs off https://api.hungrymachines.io, and every call after login carries Authorization: Bearer <access_token>.

Run these once per account:

  1. POST /auth/signup (signUp) — create the account, sending timezone (an IANA zone name) so schedules key to the right local day from the start, or send the person through signup on hungrymachines.io instead, which also sets up billing and onboarding. Programmatic signup skips both, so use it for test accounts.

  2. POST /auth/login (logIn) — exchange email and password for access_token and refresh_token.

  3. PUT /api/v1/preferences (updatePreferences) — set comfort preferences (base_temperature, optimization_mode, time_away, time_home, and so on). All fields are optional; send only what the person configured.

  4. POST /api/v1/appliances (createAppliance) — register each appliance: an appliance_type from hvac, ev_charger, home_battery, water_heater, solar, dehumidifier, robot, a name, and a type-specific config. Every type but solar requires config.entity_id — the entity your client reads sensors from and writes commands to. An HVAC unit is:

    {
    "appliance_type": "hvac",
    "name": "Living Room AC",
    "config": {
    "entity_id": "climate.living_room",
    "hvac_type": "central_ac",
    "home_size_sqft": 2000
    }
    }

    The response is {"appliance_id": "<uuid>"} — keep it, readings and per-appliance routes are keyed by it. Each type’s full config table is on Appliances & Unified Schedules.

  5. For EV chargers, batteries and water heaters only: POST /api/v1/appliances/{appliance_id}/constraints (setApplianceConstraints) — target charge, minimum charge, and deadline. HVAC comfort lives in step 3 and in per-appliance preferences, not here.

Once setup is done, run four recurring tasks:

  • Every ~5 minutes — push readings. HVAC appliances: POST /api/v1/readings (pushReadings). Every other appliance type: POST /api/v1/appliances/{appliance_id}/readings (pushApplianceReadings). This is the input the optimizer learns from; the more consistently it arrives, the better the schedules get.
  • Once a day — push the weather forecast. POST /api/v1/weather (pushWeather) sends the home’s local hourly forecast. Skip it and the optimizer plans against a generic fallback for that location, which is the single largest avoidable loss of accuracy in a custom client.
  • After the overnight run — pull the schedule. GET /api/v1/schedules (getSchedules) returns today’s schedule for every registered appliance in one call. Apply the command for the current slot locally; re-check which slot you’re in every 30 minutes.
  • Every few minutes — check for a newer schedule. GET /api/v1/schedules/updated-at returns one timestamp. 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. Keep the last value you saw and pull the schedule again when it changes.

If the person changes a preference or constraint and the UI should reflect the new schedule immediately, call POST /api/v1/schedule/recompute (recomputeSchedule) instead of waiting for the next scheduled run. It replans while you wait (usually a few seconds; allow about 20 seconds per HVAC unit) and returns date, the same appliances entries as getSchedules, and a results array. It does not return the top-level optimization_enabled, integration_health or divergence_summary.

  • Apply schedules as given. Each schedule is 48 commands, one per 30-minute slot. Don’t reinterpret, smooth, or second-guess a slot’s value — apply it and move to the next.
  • Slot 0 is local midnight. Convert your own clock to the home’s local time to find the current slot (slot = hour * 2 + (1 if minute >= 30 else 0)); every timestamp the API sends or receives is UTC, but slot indexing is local.
  • Handle both signup response shapes. POST /auth/signup returns either an authenticated session (access_token and friends) or, when the account requires email confirmation, only a pending user row with no session fields. Branch on whether access_token is present.
  • Handle source: "defaults". An appliance with no stored plan yet is flagged "defaults". On GET /api/v1/schedules its schedule is an empty object, {}: there are no setpoints and no comfort band, so send that appliance no commands. 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" with setpoint_temps populated. The single-unit route GET /api/v1/schedule behaves differently: its defaults response carries a band in high_temps / low_temps with setpoint_temps: null, and there you apply the midpoint, (high_temps[slot] + low_temps[slot]) / 2.
  • Check for a key before reading it. On GET /api/v1/schedules a schedule key is present with a value or absent, never null. Read it defensively (schedule.get("setpoint_temps"), schedule.setpoint_temps ?? null) — indexing it blind raises on exactly the accounts a new client is most likely to be testing against. The same holds for temp_trajectory, hvac_mode_schedule and fan_mode_schedule.
  • Gate on optimization_enabled. Every entry carries the flag. Do not apply an entry’s schedule while it is false.
  • Send client: {kind: "custom", name: "<your client>"} on your readings payload. POST /api/v1/readings accepts an optional client block; kind also takes hacs and node — the Hungry Machines Home Assistant integration and the Home Hub — but "custom" is the honest value for your own client, and name is a free-text label for it. The spec’s example shows kind: "hacs" only because that example is the integration’s own payload.

What your client owns vs. what the API owns

Section titled “What your client owns vs. what the API owns”

Your client owns local device I/O: reading sensors, writing setpoints and on/off commands to hardware, hysteresis, and any safety limits. The API owns the optimization: given your readings, preferences and constraints, it decides what the schedule should be. Don’t try to recompute or validate that decision locally — apply it.

Two optional command arrays need a rule your client has to get right, because applying them literally breaks real hardware:

  • fan_mode_schedule (present when fan control applies to the unit: its optimize_hvac_fan preference is on and it reports distinct fan speeds, it is registered as a window_ac, or it is a warm-up day) holds 48 values from low, high, auto. auto and off are sentinels meaning “leave the unit’s fan alone” — they are not commands. Send a fan command only for a concrete low or high, and only when the entity’s own fan-mode vocabulary has that option. On many mini-splits and Tuya-style thermostats, switching the fan to Auto engages an Eco preset that overrides the setpoint you just applied.
  • hvac_mode_schedule (present when mode control applies to the unit: its optimize_hvac_mode preference is on, it is registered as a window_ac, or it is a warm-up day) holds per-slot modes. When it is present, apply it; the only modes the API ever commands are COOL, HEAT and OFF, and here OFF is a real command.

Both arrays come only from GET /api/v1/schedules and the per-appliance schedule route, never from GET /api/v1/schedule.

A fresh account has no readings yet, so you can verify the whole loop without waiting days for real data:

  1. Create an account and log in (or use an existing test account).
  2. PUT /api/v1/preferences with a comfort band.
  3. POST /api/v1/appliances to register one HVAC appliance.
  4. POST /api/v1/readings with a handful of readings for that appliance.
  5. POST /api/v1/schedule/recompute.

Expect results to list the HVAC appliance with status: "ok", and its entry in appliances to read source: "optimization" with a 48-value setpoint_temps. savings_pct can be 0.0 on an account with only a few readings; that is a correct result, not a bug in your client. If results comes back empty, the run could not start for the account, and the entry still reads "schedule": {} with source: "defaults". Your client should handle both.

Paste this into a coding agent to start a client from scratch:

Build a client for the Hungry Machines API.
Spec: https://docs.hungrymachines.io/openapi.json (OpenAPI 3.1 — use it to
generate request/response types, or read it directly)
Full docs as plain text: https://docs.hungrymachines.io/llms-full.txt
Base URL: https://api.hungrymachines.io
Follow the contract exactly:
- Create the account with POST /auth/signup, sending timezone (an IANA
zone name, e.g. "America/Los_Angeles") so schedules key to the right
local day from the start.
- Log in with POST /auth/login, refresh with POST /auth/refresh on a 401,
and send Authorization: Bearer <access_token> on every other call.
- Set preferences with PUT /api/v1/preferences, then register appliances
with POST /api/v1/appliances (appliance_type "hvac" plus a config whose
entity_id names the local climate entity), in that order.
- Push readings roughly every 5 minutes (POST /api/v1/readings for HVAC,
POST /api/v1/appliances/{appliance_id}/readings for everything else).
On POST /api/v1/readings, send the optional "client" block as
{"kind": "custom", "name": "<your client>"}.
- Push the home's hourly forecast once a day with POST /api/v1/weather.
- Pull the day's schedule with GET /api/v1/schedules after the overnight
run, poll GET /api/v1/schedules/updated-at every few minutes and pull
again when its value changes. Apply each 30-minute slot's command as
given, without reinterpreting it. Slot 0 is local midnight. Skip any
entry whose optimization_enabled is false.
- Handle both response shapes from POST /auth/signup (session vs. pending
email confirmation).
- Handle source: "defaults": on GET /api/v1/schedules the appliance's
schedule is an empty object, so send it no commands. Schedule keys on
that route are present or absent, never null; check before reading.
- If hvac_mode_schedule is present, apply its per-slot "OFF"/"COOL"/"HEAT".
If fan_mode_schedule is present, send a fan command only for "low" or
"high"; "auto" and "off" there mean "do not send a fan command".
Do not make any claims about energy savings or how the schedule is
computed — treat the schedule as an opaque result to apply.