HTTP API¶
This page documents the HTTP API exposed by the MAC server.
The OpenAPI spec is also available at /openapi.json on a running server,
with interactive documentation at /docs (Swagger UI) and /redocs (ReDoc).
- GET /api/¶
API index route.
Returns a placeholder message.
- Status Codes:
200 OK – Response body for GET /api/.
- DELETE /api/machine/locked_out/{machine_name}¶
Set or clear machine lockout state.
POST to lock out a machine (prevent all use). DELETE to unlock the machine.
- Parameters:
machine_name (string)
- Status Codes:
200 OK – Generic success response.
404 Not Found – Generic error response.
500 Internal Server Error – Generic error response.
503 response body for the oops and locked_out endpoints when persisting machine state to disk exceeds
STATE_SAVE_TIMEOUT_SEC.The in-memory state mutation (and any fire-and-forget Slack notification) has already taken effect; only the pickle write timed out. The next successful save will catch up.
- POST /api/machine/locked_out/{machine_name}¶
Set or clear machine lockout state.
POST to lock out a machine (prevent all use). DELETE to unlock the machine.
- Parameters:
machine_name (string)
- Status Codes:
200 OK – Generic success response.
404 Not Found – Generic error response.
500 Internal Server Error – Generic error response.
503 response body for the oops and locked_out endpoints when persisting machine state to disk exceeds
STATE_SAVE_TIMEOUT_SEC.The in-memory state mutation (and any fire-and-forget Slack notification) has already taken effect; only the pickle write timed out. The next successful save will catch up.
- DELETE /api/machine/oops/{machine_name}¶
Set or clear machine Oops state.
POST to set the machine into Oops (maintenance needed) state. DELETE to clear the Oops state.
- Parameters:
machine_name (string)
- Status Codes:
200 OK – Generic success response.
404 Not Found – Generic error response.
500 Internal Server Error – Generic error response.
503 response body for the oops and locked_out endpoints when persisting machine state to disk exceeds
STATE_SAVE_TIMEOUT_SEC.The in-memory state mutation (and any fire-and-forget Slack notification) has already taken effect; only the pickle write timed out. The next successful save will catch up.
- POST /api/machine/oops/{machine_name}¶
Set or clear machine Oops state.
POST to set the machine into Oops (maintenance needed) state. DELETE to clear the Oops state.
- Parameters:
machine_name (string)
- Status Codes:
200 OK – Generic success response.
404 Not Found – Generic error response.
500 Internal Server Error – Generic error response.
503 response body for the oops and locked_out endpoints when persisting machine state to disk exceeds
STATE_SAVE_TIMEOUT_SEC.The in-memory state mutation (and any fire-and-forget Slack notification) has already taken effect; only the pickle write timed out. The next successful save will catch up.
- POST /api/machine/update¶
API method to update machine state.
Accepts POSTed JSON containing the following key/value pairs:
machine_name(string) - name of the machine sending the updateoops(boolean) - whether the oops button is pressedrfid_value(string) - value of the RFID fob/card that is currentlypresent in the machine, or empty string if none present. Note that ESPHome strips leading zeroes from this, so inside this method it is left-padded with zeroes to a length of 10 characters.
uptime(float) - uptime of the ESP32 (MCU).wifi_signal_db(float) - WiFi signal strength in dBwifi_signal_percent(float) - WiFi signal strength in percentinternal_temperature_c(float) - internal temperature of the ESP32 in°C.
amps(float; optional) - amperage value from the current clampammeter, if present, or 0.0 otherwise.
- Status Codes:
200 OK – Response body for POST /api/machine/update (200).
404 Not Found – Generic error response.
500 Internal Server Error – Generic error response.
503 Service Unavailable – Generic error response.
- GET /api/machines¶
List all machines and their current status.
Read-only endpoint returning the status of every configured machine, sorted by name. Intended for external consumers (e.g. the Equipment Status Board) to poll or reconcile machine state.
- Status Codes:
200 OK – Response body for GET /api/machines (200).
- POST /api/reload-users¶
Reload users configuration.
Hot-reloads users.json without requiring a server restart. Returns counts of removed, updated, and added users.
- Status Codes:
200 OK – Response body for POST /api/reload-users (200).
500 Internal Server Error – Generic error response.
- GET /metrics¶
API method to return Prometheus-compatible metrics.
State Save Timeout (HTTP 503)¶
The endpoints that mutate machine state —
POST /api/machine/update,POST /api/machine/oops/<machine_name>andDELETE /api/machine/oops/<machine_name>,POST /api/machine/locked_out/<machine_name>andDELETE /api/machine/locked_out/<machine_name>
— bound the time spent persisting state to disk to
STATE_SAVE_TIMEOUT_SEC (2.0 seconds). If the underlying disk hangs
and the write exceeds this budget, the handler returns:
For POST /api/machine/update (firmware-facing):
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
{"error": "state save timeout"}
For the admin endpoints (POST/DELETE on /api/machine/oops/<name>
and /api/machine/locked_out/<name>), the response body includes
action_applied: true to indicate that the requested state mutation
took effect in memory even though persistence timed out — these
actions also send Slack notifications in a fire-and-forget fashion,
which cannot be rolled back, so the action is live and the next
successful save will catch up:
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
{"error": "state save timeout", "action_applied": true}
For the firmware-facing endpoint, the 503 response is the recommended
path for the MCU to recover from a stuck disk on the server: it sees a
clean error, leaves its current relay state alone, and retries on its
next 10-second heartbeat. Without this bound, a slow-but-eventually
successful 200 response can wedge the firmware via ESPHome
http_request issues such as #6677.
Each timeout increments the per-machine mac_state_save_timeouts_total
Prometheus counter. A Slack notification is posted to
SLACK_CONTROL_CHANNEL_ID exactly once, on the transition from 1
to 2 lifetime timeouts for a given machine; subsequent timeouts under a
sustained disk hang do not re-page (operators monitoring the Prometheus
counter can alert on continued growth).
Second Relay Protocol Additions¶
POST /api/machine/update accepts an optional additive request field and emits
an additive response field to support the per-machine second_relay
configuration (see Second Relay (second_relay) — Optional):
Request —
second_relay_state(boolean, optional): The actual current state of the second relay as known to the MCU. Reported by firmware that drives a second relay; older firmware omits this field. The server uses this for observability only and never for authorization decisions.Response —
second_relay(boolean, always emitted, defaults tofalse): Desired state of the second relay. For machines withoutsecond_relayconfigured this is alwaysfalse. Firmware that does not know about this field simply ignores it.
The display field is byte-identical between pre-feature and post-feature
servers for any given (machine, operator, machine state) tuple. second_relay
configuration never causes LCD changes.
Machine Status API¶
GET /api/machines
Returns the current status of every configured machine as JSON, sorted by machine name. This read-only endpoint is intended for external consumers (such as the Equipment Status Board) to poll or reconcile machine state. It is included in the OpenAPI spec above. Each machine entry has the shape:
{
"name": "planer",
"display_name": "Planer",
"status": "in_use",
"relay": true,
"oops": false,
"locked_out": false,
"current_user": {"account_id": "123", "full_name": "Jane Doe"},
"last_checkin": 1720000000.0,
"last_update": 1720000000.0
}
status is a derived summary — one of locked_out, oops, in_use,
idle, or unknown (never checked in). current_user is null when
no user is logged in. last_checkin and last_update are epoch seconds:
last_checkin is null until the machine’s first check-in, and
last_update is null until its first meaningful state change. These are
independent — a machine that has only sent idle heartbeats has a last_checkin
while last_update is still null.
Status-Change Webhook¶
When the STATUS_WEBHOOK_URL environment variable is set (see
Environment Variables), the server POSTs a JSON webhook to that URL on
every meaningful machine status change — never on ordinary MCU heartbeats.
This lets an external consumer (such as the Equipment Status Board) maintain
its own activity history and live status without polling.
The webhook fires for these event values: login, logout,
unauthorized (known user lacking authorization), unknown_fob,
override_login, oops, unoops, lockout, unlock, and
reboot (MCU reboot detected).
The request body is the same per-machine object as GET /api/machines (so
current_user means the same thing) plus three fields:
event— the status-change event name (see above).timestamp— epoch seconds when the event fired.user— the user involved in this event (the actor), as{"account_id", "full_name"}ornull. This differs fromcurrent_userfor events likelogoutandunauthorizedwhere a user acted but is not (or is no longer) logged in.
POST <STATUS_WEBHOOK_URL>
Content-Type: application/json
{
"name": "planer",
"display_name": "Planer",
"status": "idle",
"relay": false,
"oops": false,
"locked_out": false,
"current_user": null,
"last_checkin": 1720000000.0,
"last_update": 1720000000.5,
"event": "logout",
"timestamp": 1720000000.5,
"user": {"account_id": "123", "full_name": "Jane Doe"}
}
Delivery is fire-and-forget so it never blocks the MCU response: each webhook
is sent on a background task that retries with exponential backoff and a
per-attempt timeout, giving up (and logging an error) after a few attempts. No
authentication header is sent. Consumers should treat delivery as best-effort
and reconcile via GET /api/machines when needed. See
dm_mac.webhook for details.
Prometheus Metrics¶
GET /metrics
Returns Prometheus-compatible metrics in text/plain format. This endpoint
is not included in the OpenAPI spec as it does not return JSON.
For machines with second_relay configured, the following additional
metrics are emitted (one sample per machine, with labels machine_name,
display_name, and second_relay_alias):
machine_second_relay_state— whether the second relay is currently energized (0/1)machine_second_relay_configured— always1for machines with asecond_relayblockmachine_second_relay_unauth_warn_only— theunauthorized_warn_onlyconfig flag (0/1)machine_second_relay_always_enabled— thealways_enabledconfig flag (0/1)
These metrics are not emitted at all for machines without second_relay,
keeping cardinality minimal.
The mac_state_save_timeouts_total counter (one sample per machine)
exposes the lifetime count of writes to the per-machine pickle that
exceeded STATE_SAVE_TIMEOUT_SEC. See State Save Timeout (HTTP 503)
above for the firmware-facing behavior.
See dm_mac.views.prometheus for details on the available metrics.