Situation counter
Spot and count one camera situation, like a pot boiling over or a dog licking itself.
Documentation revision 1 · Contract reference API 54 · Installed readiness unknown
Import to my phone One file, one approval on the phone: it sets up the watch, the card and keeps running with no agent. Applet file
Preview offline card · Test it in the card simulator · Complete HTML interaction body · Machine-readable recipe
What runs where
- Timer → camera → free local change check → Clef yes/no on changed photos → episode tally on the phone → watch.matched event and, for alert situations, an urgent owner notice.
- The phone expands situation into the tuned question, the tally (track = the situation id, or "situation" for own words) and the alert rule; unchanged photos reuse the last answer for free.
Controller integration
- Optional: read GET /v1/watches/{id}/tally to archive day totals; the card and the count work with no agent.
- Pick the situation with the owner: a library id from GET /v1/situations or the owner's own words.
Only Clef photo checks leave the phone (model left out, so Clef is the default for a camera watch); direct or relay transport still requires a running reachable phone.
Boundaries
- A count is sampled photos judged by a model, not continuous coverage or proof: gaps, a locked camera or the thing being out of frame are unknown, not zero.
- It does not diagnose health or detect fire: a smoke or boiling-over count is a nudge, not a safety alarm.
Lifecycle
start
POST /v1/watches with the start example; retain the returned watch_id. Then render/post the card with resource_id.
update
GET /v1/watches/{id}/tally first. To change the situation or timing, archive the tally, DELETE the old watch, POST the revised body. No watch PATCH exists.
pause
POST /v1/watches/{id}/pause with {"paused":true}; resume with false. Confirm user_paused from the returned metadata. Expiry is unchanged.
stop
DELETE /v1/watches/{id}; confirm deleted equals the saved id and watch.ended reason deleted. Archive needed tally BEFORE deletion. Cancel the interaction separately.
Failures and costs
- Own words that look like an id (lowercase_with_underscores) but aren't in the library are refused: describe it with spaces instead.
- A busy or locked camera is a gap; poor light, glare and odd angles cause misses and false counts. Aim the camera and try a known example first.
- Clef photo checks from the phone's model budget: at most max_paid_per_hour (30 here, under the 50-an-hour limit some phones have) an hour; unchanged pictures reuse the last answer for free. Read GET /v1/limits and lower the cap or lengthen interval_s for long runs.
Complete synthetic contract examples
These fixtures demonstrate contracts. They are not observations from a phone. Replace image/resource IDs with returned authorized IDs. Load request.json as the complete body of POST /v1/interactions; it contains the full offline HTML.
start
{
"id": "start",
"request": {
"method": "POST",
"path": "/v1/watches",
"body": {
"situation": "pot_boiling_over",
"data": [
"camera"
],
"camera": "rear",
"interval_s": 20,
"expires_in_s": 7776000,
"max_paid_per_hour": 30,
"local": {
"change_check": true,
"sensitivity": "normal",
"max_reuse_s": 300
}
}
},
"response": {
"status": 201,
"body": {
"watch_id": "wsituation01",
"label": "Pot boiling over",
"user_paused": false,
"created_at": "2026-11-04T10:00:00Z",
"expires_at": "2027-02-02T10:00:00Z",
"interval_s": 20,
"data": [
"camera"
],
"checks": 0,
"matches": 0,
"last_match_at": null,
"readings_held": 0,
"last_check": null,
"usage": {
"model": "clef",
"answered_locally": 0,
"paid_calls": 0,
"paid_cost_usd": 0,
"paid_calls_last_hour": 0,
"max_paid_per_hour": 30,
"skipped_over_paid_cap": 0,
"change_check": true
},
"spec": {
"interval_s": 20,
"question": null,
"data": [
"camera"
],
"expires_in_s": 7776000,
"questions": {
"pot_boiling_over": {
"instructions": "Is a pot or pan on the stove boiling over, with liquid or foam spilling over its rim? Judge from the newest photo: how likely is it that this is happening in it now?",
"type": "noul"
}
},
"paid_calls_per_hour_at_most": 30,
"label": "Pot boiling over",
"min_readings": 1,
"max_paid_per_hour": 30,
"local": {
"max_reuse_s": 300,
"learn_noise": true,
"sensitivity": "normal",
"change_check": true
},
"callback_url": null,
"cooldown_s": 600,
"repeat": true,
"context": null,
"model": "clef",
"window": 1,
"callback_when": [],
"camera": "rear",
"tally": {
"min": 0.7,
"gap_s": 180,
"kind": "noul",
"min_episode_readings": 1,
"rules": [
{
"cooldown_s": 300,
"id": "alert",
"do": {
"path": "/v1/interactions",
"method": "POST",
"body": {
"kind": "notice",
"text": "Seen on the camera just now.",
"title": "Pot boiling over",
"priority": "urgent"
}
},
"when": {
"of": "pot_boiling_over",
"within_s": 120,
"at_least": 1
}
}
],
"track": "pot_boiling_over",
"seen": null,
"labels": null,
"keep_days": 30
},
"situation": "pot_boiling_over"
},
"tally_today": null,
"rules_fired": 0,
"callback_secret": null,
"first_check_in_s": 0,
"delivery": "watch.matched and watch.ended go to your registered callback if you have one, and to GET /v1/events (both need the interactions permission); watch.rule (a rule fired) goes the same way"
}
},
"evidence": "Synthetic contract fixture, not a device observation."
}
own-words
{
"id": "own-words",
"request": {
"method": "POST",
"path": "/v1/watches",
"body": {
"situation": {
"describe": "the cat is sleeping on the armchair",
"alert": false
},
"data": [
"camera"
],
"camera": "rear",
"interval_s": 60,
"expires_in_s": 2592000,
"max_paid_per_hour": 20,
"local": {
"change_check": true,
"sensitivity": "normal",
"max_reuse_s": 900
}
}
},
"response": {
"status": 201,
"body": {
"watch_id": "wsituation02",
"label": "The cat is sleeping on the armchair",
"user_paused": false,
"created_at": "2026-11-04T10:00:00Z",
"expires_at": "2026-12-04T10:00:00Z",
"interval_s": 60,
"data": [
"camera"
],
"checks": 0,
"matches": 0,
"last_match_at": null,
"readings_held": 0,
"last_check": null,
"usage": {
"model": "clef",
"answered_locally": 0,
"paid_calls": 0,
"paid_cost_usd": 0,
"paid_calls_last_hour": 0,
"max_paid_per_hour": 20,
"skipped_over_paid_cap": 0,
"change_check": true
},
"spec": {
"interval_s": 60,
"question": null,
"data": [
"camera"
],
"expires_in_s": 2592000,
"questions": {
"situation": {
"instructions": "Does the photo show this: the cat is sleeping on the armchair? Judge from the newest photo: how likely is it that this is happening in it now?",
"type": "noul"
}
},
"paid_calls_per_hour_at_most": 20,
"label": "The cat is sleeping on the armchair",
"min_readings": 1,
"max_paid_per_hour": 20,
"local": {
"max_reuse_s": 900,
"learn_noise": true,
"sensitivity": "normal",
"change_check": true
},
"callback_url": null,
"cooldown_s": 600,
"repeat": true,
"context": null,
"model": "clef",
"window": 1,
"callback_when": [],
"camera": "rear",
"tally": {
"min": 0.7,
"gap_s": 180,
"kind": "noul",
"min_episode_readings": 1,
"rules": [],
"track": "situation",
"seen": null,
"labels": null,
"keep_days": 30
},
"situation": "situation"
},
"tally_today": null,
"rules_fired": 0,
"callback_secret": null,
"first_check_in_s": 0,
"delivery": "watch.matched and watch.ended go to your registered callback if you have one, and to GET /v1/events (both need the interactions permission); watch.rule (a rule fired) goes the same way"
}
},
"evidence": "Synthetic contract fixture, not a device observation: own words count under the track id \"situation\"."
}
stop
{
"id": "stop",
"request": {
"method": "DELETE",
"path": "/v1/watches/wsituation01"
},
"response": {
"status": 200,
"body": {
"deleted": "wsituation01"
}
},
"evidence": "Synthetic contract fixture, not a device observation."
}
Extensions
- Send {"situation": {"describe": "...", "alert": true}} to count own words and notify the owner when one starts.
- Add callback_when on the situation id, or rules, to reach a controller or show other notices; min and count_only_while tune what counts.