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

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.

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 in high_temps / low_temps, with setpoint_temps and temp_trajectory set to null. 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, never null, so read keys defensively — s.get("setpoint_temps") in Python, s.setpoint_temps ?? null in 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.

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 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 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" — it lasts only until the first plan is stored. On GET /api/v1/schedule, setpoint_temps is null and you apply the band midpoint. On GET /api/v1/schedules the appliance’s schedule is {} 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 client block on readings — its kind only 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.