Complete REST API documentation for MeterMonitor v3.1.x.
MeterMonitor supports optional API authentication via secret key.
Home Assistant Add-on:
- Authentication is disabled by default (protected by Ingress)
- Requests must originate from Ingress IP (172.30.32.2)
Standalone:
- Set
enable_auth: trueinsettings.json - Configure
secret_key(change from default!)
Include secret key in request headers:
Secret: your_secret_key_hereExample with curl:
curl -H "Secret: your_secret_key" http://localhost:8070/api/watermetersHome Assistant Add-on: http://homeassistant.local:8123/api/hassio_ingress/{token}/
- Access via Ingress (no direct port access)
- Token is managed automatically by supervisor
Standalone: http://localhost:8070/
- Default port: 8070 (configurable in
settings.json) - Host: 0.0.0.0 (all interfaces)
API Prefix: All endpoints start with /api/
{
"detail": "Error message describing what went wrong"
}| Code | Meaning | Common Causes |
|---|---|---|
| 200 | OK | Request succeeded |
| 400 | Bad Request | Invalid parameters, missing required fields |
| 401 | Unauthorized | Authentication failed or missing |
| 403 | Forbidden | IP restriction (Ingress mode) |
| 404 | Not Found | Resource doesn't exist |
| 500 | Internal Server Error | Server-side processing error |
| 502 | Bad Gateway | Home Assistant API unreachable |
GET /api/watermetersReturns all configured watermeters (setup completed).
Response:
{
"watermeters": [
[
"kitchen_meter", // name
"2026-01-28T10:30:00", // last picture timestamp
-45, // WiFi RSSI
1234567, // last history value
["base64...", "base64..."], // thresholded digits (inverted)
true // has bounding box
]
]
}GET /api/watermeters/{name}Parameters:
name: Watermeter identifier (path parameter)
Response:
{
"name": "kitchen_meter",
"picture_number": 1542,
"WiFi-RSSI": -45,
"picture": {
"format": "jpg",
"timestamp": "2026-01-28T10:30:00",
"width": 640,
"height": 480,
"length": 12543,
"data": "base64_encoded_image_data",
"data_bbox": "base64_encoded_bbox_image"
},
"dataset_present": true
}DELETE /api/watermeters/{name}Deletes watermeter and all associated data (settings, history, evaluations, sources).
Response:
{
"message": "Watermeter deleted",
"name": "kitchen_meter"
}GET /api/watermeters/{name}/historyResponse:
{
"history": [
[1234567, "2026-01-28T10:30:00", 0.95, false],
// [value, timestamp, confidence, manual]
]
}GET /api/settingsResponse:
{
"settings": [
{
"name": "kitchen_meter",
"threshold_low": 0,
"threshold_high": 100,
"threshold_last_low": 0,
"threshold_last_high": 100,
"islanding_padding": 20,
"segments": 7,
"rotated_180": false,
"shrink_last_3": false,
"extended_last_digit": false,
"max_flow_rate": 1.0,
"conf_threshold": null,
"roi_extractor": "yolo",
"template_id": null,
"segment_mode": "display",
"digit_models": null,
"decimals": 3,
"use_correctional_alg": true
}
]
}GET /api/watermeters/{name}/settings
GET /api/settings/{name}Both endpoints return the same data.
Response:
{
"threshold_low": 0,
"threshold_high": 100,
"threshold_last_low": 0,
"threshold_last_high": 100,
"islanding_padding": 20,
"segments": 7,
"shrink_last_3": false,
"extended_last_digit": false,
"max_flow_rate": 1.0,
"rotated_180": false,
"conf_threshold": null,
"roi_extractor": "yolo",
"template_id": null,
"segment_mode": "display",
"digit_models": null,
"decimals": 3,
"use_correctional_alg": true
}PUT /api/watermeters/{name}/settings
POST /api/settingsRequest Body:
{
"name": "kitchen_meter",
"threshold_low": 0,
"threshold_high": 120,
"threshold_last_low": 0,
"threshold_last_high": 100,
"islanding_padding": 20,
"segments": 7,
"rotated_180": false,
"shrink_last_3": false,
"extended_last_digit": false,
"max_flow_rate": 1.0,
"conf_threshold": 0.6,
"roi_extractor": "yolo",
"template_id": null,
"segment_mode": "display",
"digit_models": null,
"decimals": 3,
"use_correctional_alg": true
}Response:
{
"message": "Settings updated",
"name": "kitchen_meter"
}POST /api/watermeters/{name}/search_thresholdsPerforms grid search to find optimal threshold values.
Request Body:
{
"steps": 10 // 3-25, higher = more accurate but slower
}Response:
{
"threshold": 85,
"threshold_last": 90,
"avg_confidence": 0.943,
"sample_digits": ["base64...", "base64..."],
"search_time_ms": 1234
}Or on error:
{
"error": "No watermeter picture found"
}POST /api/historyRequest Body:
{
"value": 1234567,
"timestamp": "2026-01-28T10:30:00"
}Response:
{
"message": "History entry created"
}DELETE /api/history/{name}Response:
{
"message": "History deleted",
"name": "kitchen_meter"
}GET /api/watermeters/{name}/evals
GET /api/watermeters/{name}/evals?amount=50
GET /api/watermeters/{name}/evals?amount=50&from_id=1234Query Parameters:
amount: Maximum evaluations to return (optional)from_id: Return evaluations older than this ID for pagination (optional)
Response:
{
"evals": [
{
"id": 1234,
"colored_digits": ["base64...", "base64..."],
"th_digits": ["base64...", "base64..."],
"th_digits_inverted": ["base64...", "base64..."],
"predictions": [
[["5", 0.98], ["6", 0.01], ["8", 0.01]],
[["2", 0.95], ["7", 0.03], ["3", 0.02]]
],
"timestamp": "2026-01-28T10:30:00",
"result": 1234567,
"total_confidence": 0.93,
"used_confidence": 0.95,
"outdated": false,
"denied_digits": [false, false, false, false, false, false, false],
"denied_digits_count": 0,
"flow_rate_m3h": 0.012,
"delta_m3": 0.003,
"delta_raw": 3,
"time_diff_min": 15.0,
"rejection_reason": null,
"negative_correction_applied": false,
"fallback_digit_count": 0,
"digits_changed_vs_last": 1,
"digits_changed_vs_top_pred": 0,
"prediction_rank_used_counts": [7, 0, 0],
"timestamp_adjusted": false
}
]
}GET /api/watermeters/{name}/evals/{eval_id}Response: Same structure as single evaluation object above.
GET /api/watermeters/{name}/evals/countResponse:
{
"count": 1234
}DELETE /api/watermeters/{name}/evalsResponse:
{
"message": "Deleted 1234 evaluations",
"count": 1234
}POST /api/watermeters/{name}/evaluations/reevaluateProcesses the latest image with current settings.
Response:
{
"result": true
}Or on error:
{
"result": false,
"error": "No display detected in image"
}POST /api/watermeters/{name}/evaluations/mark-outdatedMarks all evaluations as outdated to trigger re-evaluation with new settings.
Response:
{
"result": true,
"count": 1234
}POST /api/watermeters/{name}/evaluations/sample
POST /api/watermeters/{name}/evaluations/sample/{offset}Returns random (offset=-1) or specific historic evaluation re-processed with current settings.
Parameters:
offset: 0 = latest, 1 = second latest, -1 = random (optional, default: -1)
Response: Same as evaluation object with additional processing metadata.
GET /api/sourcesResponse:
{
"sources": [
{
"id": 1,
"name": "kitchen_meter",
"source_type": "mqtt",
"enabled": true,
"poll_interval_s": null,
"config": null,
"last_success_ts": "2026-01-28T10:30:00",
"last_error": null,
"created_ts": "2026-01-20T08:00:00",
"updated_ts": "2026-01-28T10:30:00"
},
{
"id": 2,
"name": "bathroom_meter",
"source_type": "ha_camera",
"enabled": true,
"poll_interval_s": 600,
"config": {
"camera_entity_id": "camera.bathroom_meter",
"flash_entity_id": "light.bathroom_meter_led",
"flash_delay_ms": 1000
},
"last_success_ts": "2026-01-28T10:25:00",
"last_error": null,
"created_ts": "2026-01-22T12:00:00",
"updated_ts": "2026-01-28T10:25:00"
}
]
}POST /api/sourcesRequest Body (HA Camera):
{
"name": "bathroom_meter",
"source_type": "ha_camera",
"enabled": true,
"poll_interval_s": 600,
"config": {
"camera_entity_id": "camera.bathroom_meter",
"flash_entity_id": "light.bathroom_meter_led",
"flash_delay_ms": 1000
}
}Request Body (HTTP):
{
"name": "garage_meter",
"source_type": "http",
"enabled": true,
"poll_interval_s": 300,
"config": {
"url": "http://192.168.1.100/snapshot.jpg",
"headers": {
"Authorization": "Bearer token123"
},
"body": null
}
}Response:
{
"message": "Source created"
}PUT /api/sources/{source_id}Request Body:
{
"enabled": true,
"poll_interval_s": 900,
"config": {
"camera_entity_id": "camera.new_bathroom_meter"
}
}All fields are optional; only provided fields are updated.
Response:
{
"message": "Source updated"
}DELETE /api/sources/{source_id}Response:
{
"message": "Source deleted",
"id": 2
}POST /api/sources/{source_id}/captureManually triggers capture and processing for a source.
Response:
{
"message": "Capture and processing triggered"
}GET /api/templates/{template_id}Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "kitchen_meter_template",
"created_at": "2026-01-20T10:00:00",
"reference_image_base64": "base64_encoded_image",
"image_width": 640,
"image_height": 480,
"config": {
"display_corners": [
[100.0, 150.0],
[500.0, 150.0],
[500.0, 300.0],
[100.0, 300.0]
],
"target_width": 400,
"target_height": 150,
"target_width_ext": 480,
"target_height_ext": 180
}
}POST /api/templatesRequest Body:
{
"name": "new_meter_template",
"extractor": "orb",
"reference_image_base64": "base64_encoded_image",
"image_width": 640,
"image_height": 480,
"display_corners": [
[0.15625, 0.3125],
[0.78125, 0.3125],
[0.78125, 0.625],
[0.15625, 0.625]
]
}Note: display_corners can be in normalized (0-1) or pixel coordinates. Values ≤2.0 are treated as normalized.
Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "new_meter_template"
}GET /api/ha/statusResponse:
{
"configured": true,
"base_url": "http://supervisor/core",
"use_supervisor_token": true,
"supervisor_token_present": true,
"manual_token_present": false,
"ok": true
}GET /api/ha/camerasResponse:
{
"cameras": [
{
"entity_id": "camera.bathroom_meter",
"name": "Bathroom Meter Camera",
"suggested_flash_entity_id": "light.bathroom_meter_led"
},
{
"entity_id": "camera.garage_meter",
"name": "Garage Meter Camera",
"suggested_flash_entity_id": null
}
]
}POST /api/ha/serviceRequest Body:
{
"domain": "light",
"service": "turn_on",
"entity_id": "light.bathroom_meter_led"
}Response:
{
"result": [
{
"entity_id": "light.bathroom_meter_led",
"state": "on"
}
]
}POST /api/capture-nowRequest Body (HA Camera):
{
"cam_entity_id": "camera.bathroom_meter",
"flash_entity_id": "light.bathroom_meter_led",
"flash_delay_ms": 1000
}Request Body (HTTP):
{
"http_url": "http://192.168.1.100/snapshot.jpg",
"http_headers": {
"Authorization": "Bearer token"
},
"http_body": null
}Response:
{
"result": true,
"data": "base64_encoded_image",
"format": "jpg",
"flash_used": true
}POST /api/dataset/uploadRequest Body:
{
"name": "kitchen_meter",
"labels": ["5", "2", "3", "4", "5", "6", "7"],
"colored": ["base64...", "base64...", ...],
"thresholded": ["base64...", "base64...", ...]
}Arrays must have equal length. Labels: 0-9 or 'r'.
Response:
{
"saved": 7,
"output_root": "/data/output_dataset"
}GET /api/dataset/{name}/downloadReturns ZIP file with dataset structure.
Response: Binary ZIP data with headers:
Content-Type: application/zip
Content-Disposition: attachment; filename=kitchen_meter_dataset.zip
DELETE /api/dataset/{name}Response:
{
"message": "Dataset deleted",
"name": "kitchen_meter"
}GET /api/discoveryReturns watermeters that have not completed setup.
Response:
{
"watermeters": [
["new_meter", "2026-01-28T10:30:00", -45, "mqtt"],
// [name, timestamp, rssi, source_type]
]
}POST /api/setupRequest Body:
{
"name": "new_meter",
"picture_number": 1,
"WiFi_RSSI": -45,
"picture": {
"format": "jpg",
"timestamp": "2026-01-28T10:30:00",
"width": 640,
"height": 480,
"length": 12543,
"data": "base64_encoded_image"
}
}Response:
{
"message": "Watermeter configured",
"name": "new_meter"
}POST /api/setup/{name}/finishRequest Body:
{
"value": 1234567,
"timestamp": "2026-01-28T10:30:00"
}Response:
{
"message": "Setup completed"
}POST /api/setup/{name}/enableMoves watermeter back to discovery state.
Response:
{
"message": "Setup completed"
}GET /api/alertsReturns system-wide alerts (MQTT connection, polling errors, etc.).
Response:
{
"alerts": {
"mqtt": "Reconnecting to MQTT broker",
"polling_bathroom_meter": "Polling failed for source 'bathroom_meter': Connection timeout"
}
}GET /api/configReturns server configuration (excluding secret key).
Response:
{
"max_history": 200,
"max_evals": 100,
"mqtt": {
"broker": "172.30.32.1",
"port": 1883,
"topic": "MeterMonitor/#",
"username": "esp",
"password": "esp"
},
"allow_negative_correction": true,
"publish_to": "homeassistant/sensor/watermeter_{device}/",
"dbfile": "/data/metermonitor.db",
"homeassistant": {
"use_supervisor_token": true,
"url": "http://supervisor/core",
"request_timeout_s": 10
}
}MeterMonitor listens on the configured MQTT topic (default: MeterMonitor/#).
ESP32 devices should publish messages in this format:
Topic: MeterMonitor/{device_name}
Payload:
{
"name": "kitchen_meter",
"WiFi-RSSI": -45,
"picture": {
"timestamp": "2026-01-28T10:30:00",
"format": "jpg",
"data": "base64_encoded_image_data"
}
}Field Descriptions:
name: Unique meter identifier (alphanumeric, underscores, hyphens)WiFi-RSSI(optional): Signal strength in dBmpicture.timestamp(optional): ISO-8601 or Unix timestamp (seconds/ms). Missing/invalid values are replaced with server time.picture.format(optional): Image format hint (jpg,jpeg,png, ...). If omitted, the server detects format.picture.data(required): Base64 image bytes. Both raw base64 anddata:image/...;base64,...are accepted.
Server-generated fields:
picture_numberis managed by MeterMonitor.picture.width,picture.height,picture.lengthare derived from decoded image data.
When automatic evaluations complete (MQTT / polling / HTTP capture), the backend emits realtime events.
GET /api/ws/evaluations?secret=...
Event payload:
{
"type": "evaluation_created",
"name": "kitchen_meter",
"timestamp": "2026-03-03T14:10:00"
}Notes:
- Manual reevaluation in setup/UI does not emit this event.
- Client should refresh meter/setup views only if
namematches current meter.
MeterMonitor publishes to configured topic (default: homeassistant/sensor/watermeter_{device}/).
Discovery Topic: homeassistant/sensor/watermeter_{device}/config
Discovery Payload:
{
"name": "Kitchen Meter",
"unique_id": "watermeter_kitchen_meter_value",
"state_topic": "homeassistant/sensor/watermeter_kitchen_meter/state",
"json_attributes_topic": "homeassistant/sensor/watermeter_kitchen_meter/attributes",
"unit_of_measurement": "m³",
"device_class": "water",
"state_class": "total_increasing",
"device": {
"identifiers": ["watermeter_kitchen_meter"],
"name": "Kitchen Meter",
"model": "MeterMonitor",
"manufacturer": "MeterMonitor"
}
}State Topic: homeassistant/sensor/watermeter_{device}/state
State Payload: "0.123" (current reading in m³)
Attributes Topic: homeassistant/sensor/watermeter_{device}/attributes
Attributes Payload:
{
"confidence": 0.95,
"timestamp": "2026-01-28T10:30:00",
"unit_of_measurement": "m³"
}No built-in rate limiting. Recommended practices:
- Limit ESP32 upload frequency to 1-5 minutes
- Limit HA camera polling to 10+ minute intervals
- Avoid rapid API calls (use pagination for large datasets)
API version follows MeterMonitor version defined in config.json.
Breaking changes are documented in CHANGELOG.md.