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 are distilled from the caller’s daily data once a day, 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)

ParamRequiredTypeNotes
windowNointegerTrailing days to aggregate. Default 30, minimum 1, maximum 90. A larger value returns 400.

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 } }
}
FieldTypeDescription
latest_datestring | nullMost recent day the window is anchored on (YYYY-MM-DD); null when the customer has no history yet.
days_countedintegerNumber of days inside the window that contributed data.
savings.modeled_usdfloat | nullProjected savings the plan aimed for, summed over the window (USD).
savings.realized_usdfloat | nullSavings measured from actual consumption, summed over the window (USD). null on days without trusted per-machine power.
savings.realized_days / savings.modeled_daysintHow 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_pctfloat | nullMean daily savings percent.
savings.degeneratebooleantrue 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_idlefloat | nullTotal hours the home’s machines sat idle during the day’s priciest quarter, summed over the window.
peaks.top_peak_hours_idlefloat | nullSame 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_pctfloat | nullMean share of daily kWh that landed in the cheapest slots.
flexibility.degree_hoursfloat | nullMean daily comfort-band width, in degree-hours (thermal machines). Room the customer gave the plan to shift load.
flexibility.percent_hoursfloat | nullMean charge-window slack percent (shiftable machines — EV, battery).
comfort.in_band_home_pctfloat | nullMean 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_pctfloat | nullMean 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_eventsfloat | nullTotal manual-override events over the window.
adherence.override_by_hourarray[int] (length 24) | nullHour-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.activeintegerCount of the customer’s active machines. A real count — a 0 here is legitimate, not a missing metric.
machines.by_typeobjectPer-type count of active machines, keyed by machine type (hvac, ev_charger, home_battery, water_heater, solar).
StatusDetailCause
400"window exceeds 90 days"window greater than 90
401"Not authenticated"Missing or invalid token

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)

ParamRequiredTypeNotes
startNostring (ISO date YYYY-MM-DD)Window start, inclusive. Default end − 30 days.
endNostring (ISO date YYYY-MM-DD)Window end, inclusive. Default the caller’s local today.

The window is capped at 90 days; 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.

StatusDetailCause
400"window exceeds 90 days" / "start must be <= end"Span too wide, or reversed range
401"Not authenticated"Missing or invalid token

GET /api/v1/me/metrics/day/{date}

Section titled “GET /api/v1/me/metrics/day/”

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)

ParamRequiredTypeNotes
dateYesstring (ISO date YYYY-MM-DD)The local calendar day to render. A non-ISO value returns 400.
ParamRequiredTypeNotes
appliance_idNostring (uuid)Which HVAC to render. Defaults to the caller’s first active HVAC. Ignored if it doesn’t belong to the caller.
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 }
]
}
FieldTypeDescription
appliance_idstring | nullThe HVAC this view describes; null when the caller has no active HVAC.
setpointsarray[float] | nullThe 48-slot planned setpoints (slot 0 = local midnight). null when there is no plan for the day.
comfort_band.high / lowarray[float] | nullThe 48-slot comfort-band bounds, for display. null when absent.
price_curvearray[float] | nullThe 48-slot price curve in the feed’s own minor units (¢/kWh, pence, etc.). null when unavailable.
indoor_tempsarray[float | null]Measured indoor temperature averaged into the 48 local half-hour slots; a slot with no reading is null.
overridesarray[object]Episodes where the customer changed the plan (the plan was commanded correctly; the thermostat persistently showed something else). Each: {start, end, start_hour, readings}start / end are UTC ISO timestamps, start_hour is the local hour, readings is the run length.
conformance_defectsarray[object]Runs where the commanded setpoint itself diverged from the plan — the service’s delivery failure, kept strictly apart from overrides. Same shape. Never mix these into an override count.
StatusDetailCause
400"date must be ISO YYYY-MM-DD"Malformed date path value
401"Not authenticated"Missing or invalid 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 places the caller’s own windowed value against a precomputed distribution for the tightest comparable cohort, 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)

ParamRequiredTypeNotes
windowNointegerTrailing days the ranking window spans. Default 30, minimum 1, maximum 90. A larger value returns 400. Rankings are materialized for the 30-day window today; another window may return null ranks.

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

MetricWhat it measures
savings_pctSavings percent (percent, not dollars, so big homes on punitive plans aren’t flattered).
off_peak_kwh_share_pctShare of energy drawn in the cheapest hours.
flexibility_degree_hoursComfort-band width offered to the plan.
active_appliancesCount of connected machine types.

Cohort ladder. The caller is ranked in the tightest cohort they belong to that has enough comparable homes to rank against, walking outward: zip5zip3rate_peersfleet. cohort_kind names which tier the rank is against; 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
}
]
}
FieldTypeDescription
latest_datestring | nullThe date the ranking is anchored on (YYYY-MM-DD); null when the caller has no data.
rankings[].metricstringOne of the four ranked metrics above. All four are always present.
rankings[].cohort_kindstring | nullThe ladder tier the rank is against (zip5 / zip3 / rate_peers / fleet); null when no cohort qualifies.
rankings[].cohort_labelstring | nullGeneric, share-safe label for that cohort. Never contains ZIP digits. null when no cohort qualifies.
rankings[].rankinteger | null1-based, higher-is-better rank within the cohort. null (never 0) when the caller has no eligible value or no cohort.
rankings[].percentilefloat | nullPercent of the cohort at or below the caller’s value (higher is better). null when unranked.
rankings[].your_valuefloat | nullThe caller’s own windowed value for the metric. null when they have no eligible day.
StatusDetailCause
400"window exceeds 90 days"window greater than 90
401"Not authenticated"Missing or invalid token