Skip to content

Customer Metrics

The /api/v1/me endpoints return the signed-in customer’s own headline numbers — savings, peaks avoided, flexibility, comfort, plan adherence, connected machines, and a ranking against comparable homes. Use them to build a “what did my system do for me” dashboard.

Every query is filtered to the signed-in user: these routes return only the caller’s own figures and never expose another customer’s data. Metrics cover completed days, so a newly connected home fills in over its first days of operation.

Trailing-window aggregate of the caller’s savings, peaks avoided, flexibility, comfort, plan adherence and connected machines — one call for a headline row.

Auth: Required (Bearer JWT)

Param Required Type Notes
window No integer Trailing days to aggregate. Default 30, minimum 1, maximum 90. A value above 90 returns 400; a value below 1, or a non-integer, returns 422.

Cumulative fields (dollars saved, hours idle, override events) are summed over the window; intensity fields (percentages, degree-hours) are meaned, so those figures stay comparable regardless of how many days of data a home has.

Terminal window
curl "https://api.hungrymachines.io/api/v1/me/metrics/summary?window=30" \
-H "Authorization: Bearer YOUR_TOKEN"
{
"window": 30,
"latest_date": "2026-08-02",
"days_counted": 2,
"savings": {
"modeled_usd": 4.0,
"realized_usd": 2.0,
"realized_days": 1,
"modeled_days": 2,
"savings_pct": 20.0,
"degenerate": false
},
"peaks": {
"peak_hours_idle": 6.0,
"top_peak_hours_idle": 3.0,
"off_peak_kwh_share_pct": 55.0
},
"flexibility": {
"degree_hours": 336.0,
"percent_hours": null
},
"comfort": { "in_band_home_pct": 96.0 },
"adherence": { "conformance_pct": 95.0, "override_events": 3, "override_by_hour": [0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0] },
"machines": { "active": 2, "by_type": { "hvac": 1, "ev_charger": 1 } }
}
Field Type Description
latest_date string | null Most recent day the window is anchored on (YYYY-MM-DD); null when the customer has no history yet.
days_counted integer Number of days inside the window that contributed data.
savings.modeled_usd float | null Projected savings the plan aimed for, summed over the window (USD).
savings.realized_usd float | null Savings measured from actual consumption, summed over the window (USD). null when no day in the window could be measured.
savings.realized_days / savings.modeled_days int How many days in the window contributed each total. The two differ whenever a day could not be measured, so present the day counts alongside the sums rather than comparing them directly. 0 when nothing contributed.
savings.savings_pct float | null Mean daily savings percent.
savings.degenerate boolean true when savings_pct is ≤ 0 or ≥ 95 — a near-zero or implausibly-high figure a client should flag as a caution, not a win. false when savings_pct is null.
peaks.peak_hours_idle float | null Total hours the home’s machines sat idle during the day’s priciest quarter, summed over the window.
peaks.top_peak_hours_idle float | null Same measure over the three priciest hours only (max 3.0 per day), summed over the window. Always ≤ peak_hours_idle.
peaks.off_peak_kwh_share_pct float | null Mean share of daily kWh that landed in the cheapest slots.
flexibility.degree_hours float | null Mean daily comfort-band width, in degree-hours (thermal machines). Room the customer gave the plan to shift load.
flexibility.percent_hours float | null Mean charge-window slack percent (shiftable machines — EV, battery).
comfort.in_band_home_pct float | null Mean percent of the day the home stayed inside its comfort band. Show this alongside flexibility so the page never reads as “give up comfort to win”.
adherence.conformance_pct float | null Mean plan-delivery conformance across the customer’s machines — how reliably we delivered the plan. A shortfall is our delivery problem, kept separate from the customer’s own overrides.
adherence.override_events float | null Total manual-override events over the window.
adherence.override_by_hour array[int] (length 24) | null Hour-of-day histogram (index 0 = local midnight) of when the customer overrode the plan, summed over the window — render it as a 24-bar chart. null (never a fabricated 24 zeros) until there is histogram data. Reflects the customer’s decisions only; never combined with delivery conformance.
machines.active integer Count of the customer’s active machines. A real count — a 0 here is legitimate, not a missing metric.
machines.by_type object Per-type count of active machines, keyed by machine type (hvac, ev_charger, home_battery, water_heater, solar, dehumidifier, robot). Only types the customer has appear.
Status Detail Cause
400 "window exceeds 90 days" window greater than 90
401 "Not authenticated" / "Invalid token" / "Token expired" No Authorization header / malformed token / expired token
422 array detail window below 1, or not an integer

The same savings / peaks / flexibility / comfort metrics as /summary, but per day and un-aggregated, so a dashboard can chart a trend rather than a single number.

Auth: Required (Bearer JWT)

Param Required Type Notes
start No string (ISO date YYYY-MM-DD) Window start, inclusive. Default end − 30 days.
end No string (ISO date YYYY-MM-DD) Window end, inclusive. Default the caller’s local today.

Send both dates as YYYY-MM-DD. The window covers at most 90 calendar days inclusive of both start and end (matching /summary and /rankings’ window cap of 90); a wider span, or start after end, returns 400.

Terminal window
curl "https://api.hungrymachines.io/api/v1/me/metrics/daily?start=2026-07-04&end=2026-08-03" \
-H "Authorization: Bearer YOUR_TOKEN"
{
"start": "2026-07-04",
"end": "2026-08-03",
"days": [
{
"date": "2026-08-01",
"savings": { "modeled_usd": 1.0, "realized_usd": 0.5, "savings_pct": 10.0, "degenerate": false },
"peaks": { "peak_hours_idle": 2.0, "top_peak_hours_idle": 1.5, "off_peak_kwh_share_pct": 50.0 },
"flexibility": { "degree_hours": 336.0, "percent_hours": null },
"comfort": { "in_band_home_pct": 96.0 }
}
]
}

Per-day field meanings match the corresponding /summary fields above; savings.degenerate is computed for that single day.

Status Detail Cause
400 "window exceeds 90 days" / "start must be <= end" start more than 90 days before end, or reversed range
400 "start must be an ISO date (YYYY-MM-DD)" / "end must be an ISO date (YYYY-MM-DD)" start/end is not a valid ISO date
401 "Not authenticated" / "Invalid token" / "Token expired" No Authorization header / malformed token / expired token

One local calendar day’s plan versus actual for the caller’s HVAC: the plan the system intended (the 48-slot setpoints, the comfort band, the price curve), the measured indoor temperature, and where plan and reality diverged — split into two separate lists so an override (the customer’s own decision) is never conflated with a conformance defect (a plan the service failed to deliver).

Auth: Required (Bearer JWT)

Param Required Type Notes
date Yes string (ISO date YYYY-MM-DD) The local calendar day to render. A non-ISO value returns 400.
Param Required Type Notes
appliance_id No string (uuid) Which HVAC to render. Defaults to the caller’s first active HVAC. An id that is not one of the caller’s active HVAC appliances is ignored and the default is used; the response’s appliance_id names the unit shown.
Terminal window
curl "https://api.hungrymachines.io/api/v1/me/metrics/day/2026-06-08" \
-H "Authorization: Bearer YOUR_TOKEN"
{
"date": "2026-06-08",
"appliance_id": "9191a36c-9b61-444a-ac3a-666b7912514e",
"setpoints": [72.0, 72.0, "… 48 …"],
"comfort_band": { "high": [74.0, "… 48 …"], "low": [70.0, "… 48 …"] },
"price_curve": [12.4, 12.1, "… 48 …"],
"indoor_temps": [71.0, null, "… 48 …"],
"overrides": [
{ "start": "2026-06-08T10:10:00+00:00", "end": "2026-06-08T10:20:00+00:00", "start_hour": 10, "readings": 3 }
],
"conformance_defects": [
{ "start": "2026-06-08T12:10:00+00:00", "end": "2026-06-08T12:15:00+00:00", "start_hour": 12, "readings": 2 }
]
}
Field Type Description
appliance_id string | null The HVAC this view describes; null when the caller has no active HVAC.
setpoints array[float] | null The 48-slot planned setpoints (slot 0 = local midnight). null when there is no plan for the day.
comfort_band.high / low array[float] | null The 48-slot comfort-band bounds, for display. null when absent.
price_curve array[float] | null The 48-slot price curve in the feed’s own minor units (¢/kWh, pence, etc.). null when unavailable.
indoor_temps array[float | null] Measured indoor temperature averaged into the 48 local half-hour slots; a slot with no reading is null.
overrides array[object] Episodes where the customer changed the setpoint away from the plan. Each: {start, end, start_hour, readings} — start / end are UTC ISO timestamps, start_hour is the local hour, readings is the run length.
conformance_defects array[object] Runs where the plan was not delivered to the thermostat — the service’s delivery failure, kept strictly apart from overrides. Same shape. Never mix these into an override count.
Status Detail Cause
400 "date must be ISO YYYY-MM-DD" Malformed date path value
401 "Not authenticated" / "Invalid token" / "Token expired" No Authorization header / malformed token / expired token

How the caller stacks up against comparable homes — without revealing anything about those homes. For each of four higher-is-better metrics, the route compares the caller’s own value over the window with comparable homes, and returns only the caller’s rank, percentile and own value — never the cohort’s size, and never another home’s value.

Auth: Required (Bearer JWT)

Param Required Type Notes
window No integer Trailing days the ranking window spans. Default 30, minimum 1, maximum 90. A value above 90 returns 400; a value below 1, or a non-integer, returns 422. Use the default: rankings are available for the 30-day window, and another window may return null ranks.

Ranked metrics — all higher-is-better, so rank is 1-based with #1 the best:

Metric What it measures
savings_pct Savings as a percent, not dollars.
off_peak_kwh_share_pct Share of energy drawn in the cheapest hours.
flexibility_degree_hours Comfort-band width offered to the plan.
active_appliances Count of connected machine types.

Cohorts. The caller is ranked against the closest group of comparable homes available. cohort_kind names that group (zip5, zip3, rate_peers or fleet); cohort_label is a generic, share-safe string — "your ZIP code", "your area", "your rate plan", "all Hungry Machines homes" — that never contains raw ZIP digits or any identifier. It is safe to render on a share card.

Terminal window
curl "https://api.hungrymachines.io/api/v1/me/rankings?window=30" \
-H "Authorization: Bearer YOUR_TOKEN"
{
"window": 30,
"latest_date": "2026-08-03",
"rankings": [
{
"metric": "savings_pct",
"cohort_kind": "zip5",
"cohort_label": "your ZIP code",
"rank": 1,
"percentile": 100.0,
"your_value": 22.5
},
{
"metric": "off_peak_kwh_share_pct",
"cohort_kind": "rate_peers",
"cohort_label": "your rate plan",
"rank": 3,
"percentile": 62.5,
"your_value": 55.0
},
{
"metric": "flexibility_degree_hours",
"cohort_kind": "fleet",
"cohort_label": "all Hungry Machines homes",
"rank": null,
"percentile": null,
"your_value": null
},
{
"metric": "active_appliances",
"cohort_kind": "zip5",
"cohort_label": "your ZIP code",
"rank": 1,
"percentile": 100.0,
"your_value": 2.0
}
]
}
Field Type Description
latest_date string | null The date the ranking is anchored on (YYYY-MM-DD); null when the caller has no data.
rankings[].metric string One of the four ranked metrics above. All four are always present.
rankings[].cohort_kind string | null The cohort the rank is against (zip5 / zip3 / rate_peers / fleet); null when no cohort qualifies.
rankings[].cohort_label string | null Generic, share-safe label for that cohort. Never contains ZIP digits. null when no cohort qualifies.
rankings[].rank integer | null 1-based, higher-is-better rank within the cohort. null (never 0) when the caller has no eligible value or no cohort.
rankings[].percentile float | null Percent of the cohort at or below the caller’s value (higher is better). null when unranked.
rankings[].your_value float | null The caller’s own windowed value for the metric. null when they have no eligible day.
Status Detail Cause
400 "window exceeds 90 days" window greater than 90
401 "Not authenticated" / "Invalid token" / "Token expired" No Authorization header / malformed token / expired token
422 array detail window below 1, or not an integer