Cookwala / SDK and scenarios / 037
Hot holding: the trace, the band, the two-hour limit
Hot holding keeps cooked food at 60 to 85 degC. The envelope check reads the probe trace; the limits pack says what counts as time out of temperature control and what the device does about it.
Read first: /docs/CORE/ /docs/QUICKSTART/
Steps
A pot held at 75, 68 and 63 degC over an hour: inside the band.
checkEnvelopeThe same pot falling to 55 degC: left the envelope.
checkEnvelopeThe default pack: below 60 degC for more than two hours the food is discarded or reheated; the device alerts.
safetyLimitsThe vocabulary names the hazard: time and temperature.
getOpsWith a core probe the hold is verified by the probe; the ladder falls back to estimate and time for devices without one.
ladderThe probe rung.
Outcome
You can audit a holding log with the same four words a device uses and point to the limit and its source. The humanitarian profile (scenarios 060 onwards) applies the same rule to distributions.
Command line
printf '[{"t": 0, "tempC": 75}, {"t": 30, "tempC": 68}, {"t": 60, "tempC": 63}]' > trace.json && cookwala envelope cw.op.hold trace.json
printf '[{"t": 0, "tempC": 75}, {"t": 30, "tempC": 62}, {"t": 60, "tempC": 55}]' > trace.json && cookwala envelope cw.op.hold trace.json
curl -s http://localhost:7878/v1/safety-limitsRecorded output
Status: ok
[1] checkEnvelope: {"envelopeOk": true, "targetOk": null, "reason": "ok"}
[2] checkEnvelope: {"envelopeOk": false, "targetOk": null, "reason": "left_envelope"}
[3] safetyLimits: {"core": "0.2.0", "kind": "SafetyLimits", "id": "cookwala-default", "version": "0.2.0", "status": "draft", "limits": [{"id": "oil.max_temp", "kind": "max_temp", "action": "cut_heat", "message": "Cut heat above 200 °C: most cooking oils smoke and fire risk rises.", "source": "Common frying-oil smoke points; fire-safety practice", "appliesTo": {"medium": "oil", "ops": ["cw.op.fry", "cw.op.deep_fry"]}, "max": 200, "unit": "degC"}, {"id": "pan.max_surface", "kind": "max_temp", "action": "cut_heat", "message": "Cut heat above 280 °C pan surface (coatings degrade, oil ignites).", "source": "Cookware…
[4] getOps: {"name": "ops", "version": "0.2.0", "entries": [{"id": "cw.op.dispense", "label": {"en": "Dispense / measure", "ar": "قياس وإضافة"}, "definition": "Measure an ingredient by mass, volume or count and add it to a target.", "classes": ["cw.opfamily.handle"], "paramsSchema": {"type": "object", "properties": {"precision": {"type": "number"}}}, "sensors": ["cw.sense.mass"], "links": {"ieee1872": "IEEE 1872.1-2024 Task (to be mapped)"}}, {"id": "cw.op.transfer", "label": {"en": "Transfer", "ar": "نقل"}, "definition": "Move food or a vessel's contents from one place to another.", "classes": ["cw.opfam…
[5] ladder: "cw.sense.core_temp"Code
Run: python scenarios/out/037-hot-holding-trace/python.py
# Scenario 037: Hot holding: the trace, the band, the two-hour limit
# Hot holding keeps cooked food at 60 to 85 degC. The envelope check reads the probe trace; the limits pack says what counts as time out of temperature control and what the device does about it.
# Run a hub first: python hub/cookwala_hub.py --recipes examples
import json, os
from cookwala.client import CookwalaClient, CookwalaProblem
def load(path):
with open(path, encoding="utf-8") as f:
return json.load(f)
def pick(obj, path):
for part in path.replace("]", "").replace("[", ".").split("."):
obj = obj[int(part)] if part.isdigit() else (len(obj) if part == "length" else obj.get(part))
return obj
c = CookwalaClient(os.environ.get("COOKWALA_HUB", "http://localhost:7878"))
# Step 1: A pot held at 75, 68 and 63 degC over an hour: inside the band.
ok = c.check_envelope("cw.op.hold", [{"t": 0, "tempC": 75}, {"t": 1800, "tempC": 68}, {"t": 3600, "tempC": 63}])
assert pick(ok, "envelopeOk") == True, pick(ok, "envelopeOk")
print("ok", "envelopeOk", "=", True)
assert pick(ok, "reason") == "ok", pick(ok, "reason")
print("ok", "reason", "=", "ok")
# Step 2: The same pot falling to 55 degC: left the envelope.
cold = c.check_envelope("cw.op.hold", [{"t": 0, "tempC": 75}, {"t": 1800, "tempC": 62}, {"t": 3600, "tempC": 55}])
assert pick(cold, "envelopeOk") == False, pick(cold, "envelopeOk")
print("ok", "envelopeOk", "=", False)
assert pick(cold, "reason") == "left_envelope", pick(cold, "reason")
print("ok", "reason", "=", "left_envelope")
# Step 3: The default pack: below 60 degC for more than two hours the food is discarded or reheated; the device alerts.
limits = c.safety_limits()
assert pick(limits, "limits[7].id") == "hold.hot", pick(limits, "limits[7].id")
print("ok", "limits[7].id", "=", "hold.hot")
assert pick(limits, "limits[7].kind") == "time_temp", pick(limits, "limits[7].kind")
print("ok", "limits[7].kind", "=", "time_temp")
assert pick(limits, "limits[7].min") == 60, pick(limits, "limits[7].min")
print("ok", "limits[7].min", "=", 60)
assert pick(limits, "limits[7].holdFor") == "PT2H", pick(limits, "limits[7].holdFor")
print("ok", "limits[7].holdFor", "=", "PT2H")
assert pick(limits, "limits[7].action") == "alert", pick(limits, "limits[7].action")
print("ok", "limits[7].action", "=", "alert")
assert pick(limits, "limits[7].source") == "WHO Five Keys to Safer Food", pick(limits, "limits[7].source")
print("ok", "limits[7].source", "=", "WHO Five Keys to Safer Food")
# Step 4: The vocabulary names the hazard: time and temperature.
ops = c.get_ops()
assert pick(ops, "entries[49].id") == "cw.op.hold", pick(ops, "entries[49].id")
print("ok", "entries[49].id", "=", "cw.op.hold")
assert pick(ops, "entries[49].envelope.tempC.min") == 60, pick(ops, "entries[49].envelope.tempC.min")
print("ok", "entries[49].envelope.tempC.min", "=", 60)
assert pick(ops, "entries[49].envelope.tempC.max") == 85, pick(ops, "entries[49].envelope.tempC.max")
print("ok", "entries[49].envelope.tempC.max", "=", 85)
assert pick(ops, "entries[49].envelope.hazards[0]") == "cw.hz.time_temperature", pick(ops, "entries[49].envelope.hazards[0]")
print("ok", "entries[49].envelope.hazards[0]", "=", "cw.hz.time_temperature")
# Step 5: With a core probe the hold is verified by the probe; the ladder falls back to estimate and time for devices without one.
probe = c.ladder("cw.op.hold", ["cw.sense.core_temp"], True, False)
# Step 6: The probe rung.
assert probe == "cw.sense.core_temp", probe
print("ok", "probe", "=", "cw.sense.core_temp")
print("scenario complete")