Build Your Own Client
This is the most advanced path. Take it when Home Assistant isn’t an option — you’re integrating Hungry Machines into a mobile app, an ESP32 firmware, a Python script on a Pi, or any other platform. You’ll be responsible for the full integration loop: authentication, token refresh, sensor collection, schedule application, and any local device control.
If you have Home Assistant, the Energy Dashboard is much faster. If you want to extend Home Assistant with custom cards or REST sensors, see Custom Home Assistant integration.
Integration overview
Section titled “Integration overview”A Hungry Machines client does two things:
- Push readings — send sensor data every 5 minutes so the optimizer can learn your home’s thermal behavior
- Pull schedules — fetch optimized setpoints once daily and apply them locally
Your client handles all local device control (hysteresis, safety limits, hardware communication). The API handles optimization math.
Initial setup
Section titled “Initial setup”1. Create your account
Section titled “1. Create your account”Sign up at hungrymachines.io/signup and confirm your email. Always sign up through the website — programmatic signup skips billing and onboarding setup.
2. Get an access token
Section titled “2. Get an access token”Once your account is active, exchange credentials for a session:
curl -X POST https://api.hungrymachines.io/auth/login \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com", "password": "your-password"}'Save access_token and refresh_token from the response. Access tokens expire after 1 hour — use the refresh token to get a fresh one without re-entering credentials.
3. Set preferences
Section titled “3. Set preferences”curl -X PUT https://api.hungrymachines.io/api/v1/preferences \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "base_temperature": 72.0, "savings_level": 2, "time_away": "08:00", "time_home": "17:00", "optimization_mode": "cool" }'4. Register appliances
Section titled “4. Register appliances”Every non-solar appliance config requires an entity_id — the Home Assistant entity (or analogous identifier in your platform) the integration will read sensors from and apply setpoints/toggles to. See API Reference — Appliances for the full per-type config schema.
curl -X POST https://api.hungrymachines.io/api/v1/appliances \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "appliance_type": "hvac", "name": "Main Floor AC", "config": { "hvac_type": "central_ac", "home_size_sqft": 2000, "entity_id": "climate.main_floor" } }'5. Set constraints (non-HVAC appliances)
Section titled “5. Set constraints (non-HVAC appliances)”For EV chargers and batteries, set optimization constraints:
curl -X POST https://api.hungrymachines.io/api/v1/appliances/$APPLIANCE_ID/constraints \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "target_charge_pct": 80, "min_charge_pct": 30, "deadline_time": "07:00", "current_charge_pct": 35 }'Daily loop
Section titled “Daily loop”Once set up, your client runs two recurring tasks:
Every 5 minutes: push readings
Section titled “Every 5 minutes: push readings”Collect sensor data and POST it to the API. Include at minimum timestamp, indoor_temp, and hvac_state.
After the overnight run, and whenever it changes: pull schedule
Section titled “After the overnight run, and whenever it changes: pull schedule”After the overnight optimization has run, fetch the day’s schedule and cache it locally. Apply the corresponding setpoints every 30 minutes.
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. Poll GET /api/v1/schedules/updated-at every few minutes, keep the last value you saw, and fetch the schedule again when it changes.
The 48-interval schedule
Section titled “The 48-interval schedule”Every schedule contains 48 intervals of 30 minutes each, covering a full 24-hour day:
| Interval | Time window |
|---|---|
| 0 | 00:00–00:30 |
| 1 | 00:30–01:00 |
| 16 | 08:00–08:30 |
| 34 | 17:00–17:30 |
| 47 | 23:30–00:00 |
To find the current interval: interval = (hour * 2) + (1 if minute >= 30 else 0).
All timestamps in the API are UTC. Convert to local time before applying setpoints.
Handling source: "defaults"
Section titled “Handling source: "defaults"”source is "defaults" only until the first plan is stored for the appliance. That happens on the first recompute (a settings save, or POST /api/v1/schedule/recompute) or the next overnight run, whichever comes first. What “defaults” looks like depends on the route:
GET /api/v1/schedule(one HVAC unit, used in the examples below) returns a comfort band built from your preferences inhigh_temps/low_temps, withsetpoint_tempsandtemp_trajectoryset tonull. Apply the midpoint of the band instead:(high_temps[idx] + low_temps[idx]) / 2. Every key is always present on this route.GET /api/v1/schedules(all appliances) returns"schedule": {}for that appliance. There is no band to fall back to, so send it no commands. On this route a schedule key is present or absent, nevernull, so read keys defensively —s.get("setpoint_temps")in Python,s.setpoint_temps ?? nullin JavaScript.
From the first stored plan onward, source is "optimization" and setpoint_temps is a 48-element array every day. estimated_savings_pct can still read 0.0 on early plans, for example before any readings have arrived.
Example: Python client (httpx)
Section titled “Example: Python client (httpx)”import httpximport timefrom datetime import datetime, timezone
API = "https://api.hungrymachines.io"
class HMClient: def __init__(self, email: str, password: str): resp = httpx.post(f"{API}/auth/login", json={"email": email, "password": password}) resp.raise_for_status() s = resp.json() self.access_token = s["access_token"] self.refresh_token = s["refresh_token"] self.expires_at = s["expires_at"]
def _refresh(self): resp = httpx.post(f"{API}/auth/refresh", json={"refresh_token": self.refresh_token}) resp.raise_for_status() s = resp.json() self.access_token = s["access_token"] self.refresh_token = s["refresh_token"] self.expires_at = s["expires_at"]
def _headers(self) -> dict: # Refresh proactively with 5 minutes of headroom. if int(datetime.now(timezone.utc).timestamp()) >= self.expires_at - 300: self._refresh() return {"Authorization": f"Bearer {self.access_token}"}
def push_reading(self, indoor_temp: float, hvac_state: str, outdoor_temp: float): return httpx.post(f"{API}/api/v1/readings", headers=self._headers(), json={ "readings": [{ "timestamp": datetime.now(timezone.utc).isoformat(), "indoor_temp": indoor_temp, "hvac_state": hvac_state, "outdoor_temp": outdoor_temp, }], }).raise_for_status()
def get_schedule(self): resp = httpx.get(f"{API}/api/v1/schedule", headers=self._headers()) resp.raise_for_status() return resp.json()
def current_interval() -> int: now = datetime.now() return now.hour * 2 + (1 if now.minute >= 30 else 0)
def apply_setpoints(schedule: dict): idx = current_interval() s = schedule["schedule"] setpoints = s.get("setpoint_temps") if setpoints is not None: target = setpoints[idx] else: # Defaults path (no stored plan yet) — fall back to the band midpoint. target = (s["high_temps"][idx] + s["low_temps"][idx]) / 2 print(f"Interval {idx}: thermostat target {target}F") # TODO: send to your thermostat hardware (e.g. climate.set_temperature)
# Main loopclient = HMClient("you@example.com", "secure-password")schedule = client.get_schedule()while True: indoor = read_sensor() # your hardware function outdoor = read_outdoor_temp() # your hardware function client.push_reading(indoor, "COOL", outdoor) apply_setpoints(schedule) time.sleep(300)Persist access_token, refresh_token, and expires_at to disk so the client survives restarts without asking for credentials again.
Example: ESP32 (pseudo-code)
Section titled “Example: ESP32 (pseudo-code)”For microcontroller integrations, the pattern is the same — just adapted for constrained environments:
// ESP32 Arduino pseudo-code
#include <HTTPClient.h>#include <ArduinoJson.h>
const char* API = "https://api.hungrymachines.io";
String accessToken; // load from NVS on bootString refreshToken; // load from NVS on boot
bool refreshAccessToken() { HTTPClient http; http.begin(String(API) + "/auth/refresh"); http.addHeader("Content-Type", "application/json"); String body = "{\"refresh_token\":\"" + refreshToken + "\"}"; int code = http.POST(body); if (code == 200) { StaticJsonDocument<1024> doc; deserializeJson(doc, http.getString()); accessToken = doc["access_token"].as<String>(); refreshToken = doc["refresh_token"].as<String>(); saveToNVS(); // persist for reboots http.end(); return true; } http.end(); return false;}
void pushReading(float indoorTemp, const char* hvacState) { HTTPClient http; http.begin(String(API) + "/api/v1/readings"); http.addHeader("Authorization", "Bearer " + accessToken); http.addHeader("Content-Type", "application/json");
StaticJsonDocument<256> doc; JsonArray readings = doc.createNestedArray("readings"); JsonObject r = readings.createNestedObject(); r["timestamp"] = getISOTimestamp(); // UTC ISO 8601 r["indoor_temp"] = indoorTemp; r["hvac_state"] = hvacState;
String body; serializeJson(doc, body); int code = http.POST(body);
if (code == 401 && refreshAccessToken()) { http.end(); pushReading(indoorTemp, hvacState); // retry once return; } http.end();}
float setpointTemps[48];bool setpointTempsValid = false;float highTemps[48];float lowTemps[48];
void fetchSchedule() { HTTPClient http; http.begin(String(API) + "/api/v1/schedule"); http.addHeader("Authorization", "Bearer " + accessToken); int code = http.GET();
if (code == 200) { // Parse schedule.setpoint_temps (preferred) into setpointTemps[]; // mark setpointTempsValid = false when the field is null (defaults // path) and fall back to high_temps / low_temps for display. // Always populate highTemps[] and lowTemps[] for UI rendering. } http.end();}
void loop() { float temp = readSensor(); pushReading(temp, "COOL"); int interval = (hour() * 2) + (minute() >= 30 ? 1 : 0); float target; if (setpointTempsValid) { target = setpointTemps[interval]; } else { target = (highTemps[interval] + lowTemps[interval]) / 2.0f; } applySetpoint(target); delay(300000); // 5 minutes}Key points
Section titled “Key points”- Sign up at hungrymachines.io — programmatic signup via
/auth/signupskips billing setup; always create accounts through the website. - Two auth endpoints for tokens —
/auth/loginand/auth/refresh. Both live onapi.hungrymachines.io, both use email + password. - 1-hour access token, 30-day refresh token — the refresh token is rotated on every
/auth/refreshcall, so save the new one each time. - UTC timestamps — all API timestamps are UTC. Convert to local time for display and setpoint timing.
- Apply
setpoint_temps[idx], nothigh_temps/low_temps— the optimizer emits a per-interval target temperature directly. The high/low arrays are display-only comfort bounds. - 48 intervals — every schedule is 48 half-hour slots.
interval = hour * 2 + (minute >= 30). - Handle
source: "defaults"— it lasts only until the first plan is stored. OnGET /api/v1/schedule,setpoint_tempsisnulland you apply the band midpoint. OnGET /api/v1/schedulesthe appliance’sscheduleis{}and there is nothing to apply. - Poll
GET /api/v1/schedules/updated-at— plans are replaced during the day; fetch the schedule again when the value changes. - Omit the
clientblock on readings — itskindonly accepts the two first-party clients. - Local hysteresis — your client handles thermostat hysteresis and safety limits. The API provides target setpoints, not direct control signals.
- Rate limit — 600 readings per active HVAC or dehumidifier appliance, per account, per UTC day. Normal 5-minute reporting (288 readings a day per unit) is well within it; it only trips on runaway loops or large catch-up batches.
Next steps
Section titled “Next steps”- Authentication — full token lifecycle reference
- API Reference — Readings — exact request/response schemas
- API Reference — Schedule — 48-interval schedule shape
- API Reference — Appliances — multi-device registration + constraints
- Custom Home Assistant integration — same API from inside Home Assistant if you change your mind
- OpenAPI description — every route in this guide as one machine-readable file, for generating a typed client
- Reference Clients — copy-ready, dependency-free Python and JavaScript files covering the same loop