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.
Once daily: pull schedule
Section titled “Once daily: pull schedule”After 05:00 UTC (when the nightly optimization has completed), fetch the day’s schedule and cache it locally. Apply the corresponding setpoints every 30 minutes.
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"”When source is "defaults" in the schedule response, the optimizer doesn’t have a per-user thermal model yet. The fitter requires ~72 hourly buckets of data (about 3 days of continuous 5-min readings); a daily 06:30 UTC initial-fit job picks up new users as soon as they cross that threshold. While on defaults, the schedule contains a flat comfort band based on your preferences — safe to apply, but no energy savings.
When source is "defaults", setpoint_temps will be null. Apply the midpoint of the comfort band instead: (high_temps[idx] + low_temps[idx]) / 2.
Once the thermal model is ready, source switches to "optimization", setpoint_temps becomes a 48-element array, and you’ll see non-zero estimated_savings_pct.
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 model 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"— safe to apply but no savings yet. The optimizer produces a per-user model after ~3 days of readings (daily 06:30 UTC initial-fit pass picks new users up). When on defaults,setpoint_tempsisnull— apply the band midpoint instead. - Local hysteresis — your client handles thermostat hysteresis and safety limits. The API provides target setpoints, not direct control signals.
- Rate limit — max 300 readings per user per day across all endpoints.
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