Skip to content

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.

A Hungry Machines client does two things:

  1. Push readings — send sensor data every 5 minutes so the optimizer can learn your home’s thermal behavior
  2. 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.

Sign up at hungrymachines.io/signup and confirm your email. Always sign up through the website — programmatic signup skips billing and onboarding setup.

Once your account is active, exchange credentials for a session:

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

Terminal window
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"
}'

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.

Terminal window
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"
}
}'

For EV chargers and batteries, set optimization constraints:

Terminal window
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
}'

Once set up, your client runs two recurring tasks:

Collect sensor data and POST it to the API. Include at minimum timestamp, indoor_temp, and hvac_state.

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.

Every schedule contains 48 intervals of 30 minutes each, covering a full 24-hour day:

IntervalTime window
000:00–00:30
100:30–01:00
1608:00–08:30
3417:00–17:30
4723: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.

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.

import httpx
import time
from 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 loop
client = 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.

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 boot
String 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
}
  • Sign up at hungrymachines.io — programmatic signup via /auth/signup skips billing setup; always create accounts through the website.
  • Two auth endpoints for tokens/auth/login and /auth/refresh. Both live on api.hungrymachines.io, both use email + password.
  • 1-hour access token, 30-day refresh token — the refresh token is rotated on every /auth/refresh call, 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], not high_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_temps is null — 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.