Clawd — Off-Device Edition (n8n)
Clawd without "out of memory": runs entirely off-device via n8n, no Berry runtime needed on the AWTRIX.
| System | N8N |
|---|---|
| Firmware | AWTRIX NG |
| Topic | Gaming |
| Built by | Möpchi |
| File | 4jOumVthoORk.json · 25.6 KB |
| Icons | — |
| Published | 7 Aug 2026 · updated 7 Aug 2026 |
| Downloads | 4 |
A variant of Clawd (Blueforcer's virtual pet script) for anyone hitting ERR:Clawd - out of memory — usually on plain ESP32 boards without PSRAM, which the original is too large for.
The entire game logic (hunger, happiness, energy, cleanliness, evolution through 4 stages, sickness, death + restart, sound, notifications) runs in an n8n workflow instead of on the device. AWTRIX just renders draw commands pushed over HTTP — no on-device computation, no memory problem.
Also includes optional Home Assistant dashboard buttons (feed/play/clean/med/sleep/stats) via a webhook, in addition to the physical Ulanzi buttons.
Requirements: n8n + an MQTT broker (Home Assistant optional).
Full setup guide, workflow file, and HA config: https://github.com/Moepchi/clawd-offdevice
Full credit for the game design and original sprites goes to Blueforcer's Clawd — this is just an alternative runtime for it.
4jOumVthoORk.json
{
"name": "Clawd (AWTRIX pet, off-device)",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "seconds",
"secondsInterval": 2
}
]
}
},
"id": "trg-tick",
"name": "Tick every 2s",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-400,
0
]
},
{
"parameters": {
"jsCode": "return [{ json: { event: 'tick' } }];"
},
"id": "norm-tick",
"name": "Normalize Tick",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-160,
0
]
},
{
"parameters": {
"topics": "awtrixNG/state/buttons/+",
"options": {}
},
"id": "trg-mqtt",
"name": "Button Events (MQTT)",
"type": "n8n-nodes-base.mqttTrigger",
"typeVersion": 1,
"position": [
-400,
260
],
"credentials": {
"mqtt": {
"id": "REPLACE_WITH_YOUR_MQTT_CREDENTIAL_ID",
"name": "AWTRIX broker"
}
},
"notes": "Topic prefix 'awtrixNG' must match mqttPrefix in AWTRIX NG's MQTT settings."
},
{
"parameters": {
"jsCode": "const topic = $json.topic || '';\nconst parts = topic.split('/');\nconst btn = parts[parts.length - 1]; // 'select' | 'left' | 'right'\n// AWTRIX publishes the new button state (often \"true\"/\"1\" on press, \"false\"/\"0\" on release).\n// Only react on the press edge.\nconst raw = ($json.message || '').toString().trim().toLowerCase();\nconst isPress = raw === 'true' || raw === '1' || raw === 'on';\nif (!isPress) return [];\nreturn [{ json: { event: 'button', btn } }];"
},
"id": "norm-button",
"name": "Normalize Button",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-160,
260
],
"notes": "Check AWTRIX NG's state/buttons/<button> payload format on your firmware version and adjust the isPress check if needed."
},
{
"parameters": {
"httpMethod": "POST",
"path": "clawd-action",
"responseMode": "onReceived",
"options": {}
},
"id": "trg-webhook",
"name": "HA Action (Webhook)",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"webhookId": "clawd-action",
"position": [
-400,
460
],
"notes": "Body: {\"action\":\"feed\"|\"play\"|\"clean\"|\"med\"|\"sleep\"|\"stats\"}. Called from Home Assistant via rest_command."
},
{
"parameters": {
"jsCode": "const map = { feed: 1, play: 2, clean: 3, med: 4, sleep: 5, stats: 6 };\nconst body = $json.body || $json;\nconst id = map[(body.action || '').toString().toLowerCase()];\nif (!id) return [];\nreturn [{ json: { event: 'action', id } }];"
},
"id": "norm-action",
"name": "Normalize Action",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-160,
460
],
"notes": "Maps HA's {\"action\":\"feed\"} straight to the same action() call the on-device menu would eventually fire - no menu-cycling/dwell needed."
},
{
"parameters": {
"jsCode": "// ============================================================================\n// Clawd Engine — n8n port of the AWTRIX Berry script \"Clawd\"\n// Persists pet state in this workflow's static data, so it survives across\n// executions without an external DB. See clawd-engine.js for the annotated\n// reference copy.\n// ============================================================================\n\nconst PET_NAME = 'Clawd';\n\nconst CM = {\n o: '#D97757', O: '#E58963', q: '#A8553C', d: '#A94F38', c: '#F0906B',\n x: '#C4694D', w: '#FFFFFF', r: '#E05540', e: '#F2E3C8', s: '#C96F4A',\n y: '#FFD34D', m: '#8B5A2B', g: '#93A7C4', t: '#9AA0A6'\n};\n\nconst SPR = {\n egg: ['.eee.', 'eeeee', 'esees', 'eeeee', 'eseee', '.eee.'],\n b1: { a: ['c...c', '.ooo.', 'owowo', '.d.d.'], b: ['.....', 'coooc', 'owowo', 'd...d'] },\n b2: { a: ['c....c', '.oooo.', 'owoowo', '.oooo.', '.d..d.'], b: ['......', 'cooooc', 'owoowo', '.oooo.', 'd....d'] },\n b3: { a: ['c.....c', 'c.ooo.c', '.ooooo.', 'owooowo', '.ooooo.', '.d.d.d.'], b: ['.......', 'c.ooo.c', 'coooooc', 'owooowo', '.ooooo.', 'd.d.d.d'] },\n a0: { a: ['.y.yy.y.', 'c.OOOO.c', '.OOOOOO.', 'OOwOOwOO', '.OOOOOO.', '..OOOO..', '.d.d.d.d'], b: ['..y..y..', 'ccOOOOcc', '.OOOOOO.', 'OOwOOwOO', '.OOOOOO.', '..OOOO..', 'd.d.d.d.'] },\n a1: { a: ['cc....cc', 'c.oooo.c', '.oooooo.', 'oowoowoo', '.oooooo.', '..oooo..', '.d.d.d.d'], b: ['........', 'ccoooocc', '.oooooo.', 'oowoowoo', '.oooooo.', '..oooo..', 'd.d.d.d.'] },\n a2: { a: ['xx....xx', 'x.qqqq.x', '.qqqqqq.', 'qqrqqrqq', '.qqqqqq.', '..dddd..', '.d.d.d.d'], b: ['........', 'xxqqqqxx', '.qqqqqq.', 'qqrqqrqq', '.qqqqqq.', '..dddd..', 'd.d.d.d.'] },\n ghost: ['.gggg.', 'gwggwg', 'gggggg', 'gggggg', 'g.g.g.'],\n tomb: ['.ttt.', 'ttttt', 'tt.tt', 'tt.tt', 'ttttt', 'ttttt'],\n poop: ['.m.', 'mmm'],\n food: ['.yy.', 'mmmm', '.ee.'],\n heart: ['r.r', 'rrr', '.r.']\n};\n\nfunction pickFrame(pair) { return (Math.floor(Date.now() / 1200) % 2 === 0) ? pair.a : pair.b; }\n\nconst GLY = [\n ['.r....r.', '..r..r..', '...rr...', '...rr...', '..r..r..', '.r....r.'],\n ['........', '..yyyy..', '.yyyyyy.', '.mmmmmm.', '.rrrrrr.', '.eeeeee.'],\n ['...y....', '..yyy...', 'yyyyyyy.', '.yyyyy..', '..y.y...', '.y...y..'],\n ['......ee', '.....ee.', '....ee..', '.yee....', 'yyy.....', 'yy......'],\n ['...rr...', '...rr...', '.rrrrrr.', '.rrrrrr.', '...rr...', '...rr...'],\n ['..eee...', '.ee.....', '.ee.....', '.ee.....', '.ee.....', '..eee...'],\n ['......w.', '......w.', '...w..w.', '...w..w.', 'w..w..w.', 'w..w..w.'],\n ['.y.y.y..', '..yyy...', 'yyyyyyy.', '..yyy...', '.y.y.y..']\n];\nconst MLAB = ['EXIT', 'FEED', 'PLAY', 'CLEAN', 'MED', 'SLEEP', 'STATS'];\n\nconst SND = {\n 1: 'm:d=16,o=5,b=200:g,p,g,p,g',\n 2: 'c:d=32,o=6,b=220:e,g',\n 3: 'e:d=16,o=5,b=140:c,e,g,8c6,16p,8e6',\n 4: 'h:d=16,o=6,b=180:c,e,g,c7',\n 5: 'c:d=32,o=6,b=220:e,g',\n 6: 'x:d=16,o=4,b=200:c',\n 7: 'c:d=32,o=6,b=220:e,g',\n 8: 'w:d=16,o=6,b=180:c,e,g,8c7',\n 9: 'd:d=4,o=4,b=70:8e,8d,c'\n};\n\nfunction clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }\n\nfunction freshState(gen) {\n return {\n st: 0, ev: 0, va: 1,\n h: 10000, ha: 10000, en: 10000, cl: 10000, hp: 10000,\n sl: 0, slo: 0, sk: 0,\n age: 0, warm: 0, cs: 0, gen: gen || 1,\n pp: 0, pt: 0, cf: 0,\n ui: 0, mi: 0, dwell: 0, rp: 0,\n ev_k: 0, ev_t0: 0, stat_open: 0,\n lastNotify: 0, decAcc: 0, aslast: null,\n last_ts: Math.floor(Date.now() / 1000)\n };\n}\n\nfunction decode(rows) {\n const out = [];\n for (let y = 0; y < rows.length; y++) {\n const row = rows[y];\n for (let x = 0; x < row.length; x++) {\n const c = CM[row[x]];\n if (c) out.push([x, y, c]);\n }\n }\n return out;\n}\n\nfunction blit(draw, spriteRows, ox, oy, overrideColor) {\n for (const [x, y, c] of decode(spriteRows)) {\n draw.push(['pixel', ox + x, oy + y, overrideColor || c]);\n }\n}\n\nfunction blitBlink(draw, spriteRows, ox, oy, skinColor, blinking) {\n for (let y = 0; y < spriteRows.length; y++) {\n const row = spriteRows[y];\n for (let x = 0; x < row.length; x++) {\n const ch = row[x];\n let color = CM[ch];\n if (!color) continue;\n if (blinking && ch === 'w') color = skinColor;\n draw.push(['pixel', ox + x, oy + y, color]);\n }\n }\n}\n\nconst STARS = [[3, 0], [8, 1], [13, 0], [1, 2], [15, 2]];\n\nfunction applyCatchup(s, dtSec) {\n const el = Math.min(dtSec, 43200);\n s.age += el;\n if (s.st === 1) {\n const d = el * 3 / 10;\n s.h = clamp(s.h - d * 7 / 10, 0, 10000);\n s.ha = clamp(s.ha - d / 2, 0, 10000);\n s.en = clamp(s.en + d / 4, 0, 10000);\n s.cl = clamp(s.cl - d / 5, 0, 10000);\n if (s.pt > 0) {\n s.pt -= d;\n if (s.pt <= 0) { s.pt = 0; s.pp = Math.min(s.pp + 1, 3); s.cl = clamp(s.cl - 1500, 0, 10000); }\n }\n }\n}\n\nfunction decayStep(s) {\n if (s.st !== 1) return;\n const sl = s.sl === 1;\n const xtra = (s.pp >= 2 || s.sk === 1) ? 5 : 0;\n s.h = clamp(s.h - (sl ? 3 : 7), 0, 10000);\n s.ha = clamp(s.ha - (sl ? 0 : 5) - xtra, 0, 10000);\n s.en = clamp(s.en + (sl ? 12 : -4), 0, 10000);\n s.cl = clamp(s.cl - 2 - 4 * s.pp, 0, 10000);\n\n const vals = [s.h, s.ha, s.en, s.cl];\n let cf = s.cf;\n for (let i = 0; i < 4; i++) {\n const bit = 1 << i;\n if (vals[i] === 0) {\n if ((cf & bit) === 0) { s.cs -= 25; cf ^= bit; }\n } else if (vals[i] > 3000 && (cf & bit) !== 0) {\n cf ^= bit;\n }\n }\n s.cf = cf;\n\n if (s.sk === 0 && !sl) {\n if ((s.h < 2000 || s.ha < 2000 || s.cl < 2000 || s.pp >= 2) && Math.random() < 0.02) {\n s.sk = 1; s.cs -= 25;\n }\n }\n\n let dmg = 0;\n if (s.h === 0) dmg += 12;\n if (s.cl === 0) dmg += 6;\n if (s.sk === 1) dmg += 8;\n if (dmg > 0) s.hp = clamp(s.hp - dmg, 0, 10000);\n else s.hp = clamp(s.hp + 5, 0, 10000);\n\n if (s.hp <= 0) {\n s.st = 2; s.ev_k = 9; s.ev_t0 = Date.now(); s.ui = 0;\n }\n}\n\nfunction evolve(s, stage) {\n s.ev = stage;\n if (stage === 1) { s.st = 1; s.ev_k = 4; }\n else {\n if (stage === 4) {\n s.va = s.cs >= 200 ? 0 : (s.cs <= -100 ? 2 : 1);\n }\n s.ev_k = 3;\n }\n s.ev_t0 = Date.now();\n}\n\nfunction checkEvolution(s) {\n if (s.st !== 1) return;\n if (s.ev === 1 && s.age >= 43200) evolve(s, 2);\n else if (s.ev === 2 && s.age >= 129600) evolve(s, 3);\n else if (s.ev === 3 && s.age >= 259200) evolve(s, 4);\n}\n\nfunction advanceTime(s) {\n const now = Math.floor(Date.now() / 1000);\n const dt = Math.max(0, now - (s.last_ts || now));\n s.last_ts = now;\n\n if (s.st === 0) {\n s.age += dt;\n if (s.age + s.warm >= 1800) evolve(s, 1);\n return;\n }\n if (s.st !== 1) return;\n\n if (dt > 60) { applyCatchup(s, dt); checkEvolution(s); return; }\n\n s.age += dt;\n\n const hr = new Date().getHours();\n const auto = hr >= 22 || hr < 8;\n if (s.aslast === null || s.aslast === undefined) {\n s.aslast = auto;\n } else if (auto !== s.aslast) {\n s.aslast = auto;\n s.slo = 0;\n }\n if (s.slo === 0) s.sl = auto ? 1 : 0;\n\n if (s.pt > 0 && s.sl !== 1) {\n s.pt -= dt;\n if (s.pt <= 0) { s.pt = 0; s.pp = Math.min(s.pp + 1, 3); s.cl = clamp(s.cl - 1500, 0, 10000); }\n }\n\n s.decAcc = (s.decAcc || 0) + dt;\n const steps = Math.min(360, Math.floor(s.decAcc / 10));\n s.decAcc -= steps * 10;\n for (let i = 0; i < steps; i++) decayStep(s);\n\n checkEvolution(s);\n}\n\nfunction checkDwell(s) {\n const now = Date.now();\n if (s.ui === 1 && now - s.dwell >= 2000) {\n action(s, s.mi);\n } else if (s.ui === 5) {\n if (s.rp === 0 && now - s.dwell >= 6000) {\n s.ui = 0;\n } else if (s.rp === 1 && now - s.dwell >= 3000) {\n const gen = s.gen;\n Object.assign(s, freshState(gen + 1));\n s.ev_k = 7; s.ev_t0 = now;\n }\n } else if (s.ui === 3 && now - s.stat_open >= 8000) {\n s.ui = 0;\n }\n}\n\nfunction action(s, id) {\n s.ui = 0;\n if (id === 1) {\n if (s.sl === 1 || s.st !== 1) { s.ev_k = 6; }\n else {\n if (s.h > 9000) s.ha = clamp(s.ha - 500, 0, 10000);\n else if (s.h < 7000) s.cs += 10;\n s.h = clamp(s.h + 4000, 0, 10000);\n s.pt = 2700 + Math.floor(Math.random() * 2700);\n s.ev_k = 1;\n }\n } else if (id === 2) {\n if (s.sl === 1 || s.st !== 1 || s.en < 1500) { s.ev_k = 6; }\n else {\n s.ha = clamp(s.ha + 1500, 0, 10000);\n s.en = clamp(s.en - 800, 0, 10000);\n s.cs += 5;\n s.ev_k = 8;\n }\n } else if (id === 3) {\n if (s.sl === 1) { s.ev_k = 6; }\n else {\n if (s.pp > 0) s.cs += 10;\n s.pp = 0; s.cl = 10000; s.ev_k = 2;\n }\n } else if (id === 4) {\n if (s.sk === 1) { s.sk = 0; s.cs += 10; s.ha = clamp(s.ha - 500, 0, 10000); s.ev_k = 5; }\n else { s.ev_k = 6; }\n } else if (id === 5) {\n if (s.sl === 1) { s.slo = 1; s.sl = 0; } else { s.slo = 2; s.sl = 1; }\n } else if (id === 6) {\n s.ui = 3; s.stat_open = Date.now();\n }\n s.ev_t0 = Date.now();\n}\n\nfunction onButton(s, btn) {\n const now = Date.now();\n if (btn !== 'select') {\n if (s.ui !== 0) s.ui = 0;\n return;\n }\n if (s.st === 0) {\n s.warm = Math.min(s.warm + 60, 900);\n s.ev_k = 7; s.ev_t0 = now;\n } else if (s.st === 2) {\n if (s.ui === 5) {\n if (s.rp === 0) { s.rp = 1; s.dwell = now; }\n else { s.ui = 0; }\n } else { s.ui = 5; s.rp = 0; s.dwell = now; }\n } else if (s.ui === 0) {\n s.ui = 1; s.mi = 0; s.dwell = now;\n } else if (s.ui === 1) {\n s.mi = (s.mi + 1) % 7;\n s.dwell = now;\n } else if (s.ui === 3) {\n s.ui = 0;\n }\n}\n\nfunction renderPayload(s) {\n if (s.ui === 3) {\n const a = s.age;\n const days = Math.floor(a / 86400), hrs = Math.floor((a % 86400) / 3600);\n return {\n text: `${PET_NAME} AGE ${days}d${hrs}h GEN ${s.gen} CARE ${s.cs} HP ${Math.floor(s.hp / 100)}%`,\n textColor: '#F0E6D8', scroll: true\n };\n }\n\n const draw = [['rectFill', 0, 0, 32, 8, '#000000']];\n\n if (s.ui === 1) {\n let lab = MLAB[s.mi], glyph = GLY[s.mi];\n if (s.mi === 5 && s.sl === 1) { lab = 'WAKE'; glyph = GLY[7]; }\n blit(draw, glyph, 0, 1);\n draw.push(['text', 11, 1, lab, '#F0E6D8']);\n draw.push(['rectFill', 0, 7, 32, 1, '#1A1A1A']);\n const fw = clamp(Math.floor((Date.now() - s.dwell) * 32 / 2000), 0, 32);\n if (fw > 0) draw.push(['rectFill', 0, 7, fw, 1, '#D97757']);\n return { draw };\n }\n\n if (s.ui === 5) {\n draw.push(['rectFill', 0, 7, 32, 1, '#1A1A1A']);\n if (s.rp === 0) {\n draw.push(['text', 7, 1, 'NEW EGG?', '#8A8178']);\n } else {\n draw.push(['text', 7, 1, 'NEW EGG?', '#F0E6D8']);\n const fw = clamp(Math.floor((Date.now() - s.dwell) * 32 / 3000), 0, 32);\n if (fw > 0) draw.push(['rectFill', 0, 7, fw, 1, '#D97757']);\n }\n return { draw };\n }\n\n if (s.st === 2) {\n draw.push(['line', 0, 7, 16, 7, '#1E2430']);\n blit(draw, SPR.tomb, 4, 1);\n blit(draw, SPR.ghost, 11, (Math.floor(Date.now() / 800) % 2 === 0) ? 1 : 2);\n return { draw };\n }\n\n const hr = new Date().getHours();\n const night = s.sl === 1 || hr >= 20 || hr < 6;\n draw.push(['line', 0, 7, 16, 7, night ? '#14203A' : '#1E4D1E']);\n draw.push(['rectFill', 1, 0, 2, 2, night ? '#8C8655' : '#9A8430']);\n\n if (night) {\n const off = Math.floor(Date.now() / 400) % STARS.length;\n STARS.forEach(([sx, sy], i) => {\n if (i !== off) draw.push(['pixel', sx, sy, '#3A3A3A']);\n });\n }\n\n const blinking = (Date.now() % 4000) < 150;\n\n if (s.st === 0) {\n const wob = Math.floor(Date.now() / 700) % 2;\n blit(draw, SPR.egg, 5 + wob, 1);\n const pr = clamp(Math.floor((s.age + s.warm) * 17 / 1800), 0, 17);\n if (pr > 0) draw.push(['rectFill', 0, 7, pr, 1, '#D97757']);\n } else {\n let pair, w, h, skin;\n if (s.ev === 1) { pair = SPR.b1; w = 5; h = 4; skin = CM.o; }\n else if (s.ev === 2) { pair = SPR.b2; w = 6; h = 5; skin = CM.o; }\n else if (s.ev === 3) { pair = SPR.b3; w = 7; h = 6; skin = CM.o; }\n else if (s.va === 0) { pair = SPR.a0; w = 8; h = 7; skin = CM.O; }\n else if (s.va === 2) { pair = SPR.a2; w = 8; h = 7; skin = CM.q; }\n else { pair = SPR.a1; w = 8; h = 7; skin = CM.o; }\n const asleep = s.sl === 1;\n const frame = asleep ? pair.a : pickFrame(pair);\n const ox = Math.floor((17 - w) / 2), oy = 7 - h;\n\n for (let i = 0; i < s.pp; i++) blit(draw, SPR.poop, 12 + i, 5 - i);\n blitBlink(draw, frame, ox, oy, skin, asleep || blinking);\n if (asleep) draw.push(['text', ox + w + 1, 3, 'z', '#5C7FBF']);\n if (s.sk === 1) { draw.push(['pixel', 0, 0, '#35C24A']); draw.push(['pixel', 0, 1, '#35C24A']); }\n\n const ed = Date.now() - s.ev_t0;\n if ((s.ev_k === 1 || s.ev_k === 8) && ed < 900) blit(draw, SPR.heart, 7, 0);\n else if (s.ev_k === 2 && ed < 1200) {\n const bx = Math.floor(ed * 17 / 1200);\n draw.push(['line', bx, 2, bx, 6, '#F2E3C8']);\n } else if (s.ev_k === 5 && ed < 700) {\n draw.push(['line', 7, 1, 9, 1, '#35C24A']); draw.push(['line', 8, 0, 8, 2, '#35C24A']);\n } else if (s.ev_k === 6 && ed < 700) {\n draw.push(['line', 6, 1, 10, 5, '#E05540']); draw.push(['line', 10, 1, 6, 5, '#E05540']);\n }\n }\n\n const barBlink = (Math.floor(Date.now() / 300) % 2) === 0;\n const bars = [s.h, s.ha, s.en, s.cl];\n const bcol = ['#D97757', '#E8C33F', '#3FB4E8', '#4FC96F'];\n for (let i = 0; i < 4; i++) {\n draw.push(['rectFill', 18, i * 2, 14, 1, '#202020']);\n let w = Math.floor(bars[i] * 14 / 10000);\n if (w < 1 && bars[i] > 0) w = 1;\n if (w > 0 && (bars[i] >= 2500 || barBlink)) {\n draw.push(['rectFill', 18, i * 2, w, 1, bcol[i]]);\n }\n }\n return { draw };\n}\n\nfunction checkNotify(s) {\n if (s.st !== 1 || s.sl === 1) return null;\n const now = Date.now();\n if (now - (s.lastNotify || 0) < 1800000) return null;\n if (s.h < 1500 || s.ha < 1500 || s.cl < 1500 || s.sk === 1) {\n s.lastNotify = now;\n return { text: `${PET_NAME} needs you!`, textColor: '#D97757' };\n }\n return null;\n}\n\n// ---- run once per incoming item -----------------------------------------\nconst staticData = $getWorkflowStaticData('global');\nif (!staticData.clawd) staticData.clawd = freshState(1);\nconst s = staticData.clawd;\n\nconst input = $input.item.json;\nconst prevEvK = s.ev_k;\nconst prevSt = s.st;\nconst prevEv = s.ev;\nconst prevSk = s.sk;\n\nadvanceTime(s);\ncheckDwell(s);\nif (input.event === 'button') onButton(s, input.btn);\nelse if (input.event === 'action') action(s, input.id);\n\nconst payload = renderPayload(s);\nconst sound = (s.ev_k !== prevEvK && s.ev_k > 0) ? SND[s.ev_k] : null;\nconst notify = checkNotify(s);\n\nconst diedNow = prevSt !== 2 && s.st === 2;\nconst evolvedNow = prevEv !== s.ev;\nconst gotSickNow = prevSk !== 1 && s.sk === 1;\nconst switchTo = input.event !== 'tick' || diedNow || evolvedNow || gotSickNow;\n\nreturn [{ json: { payload, sound, notify, switchTo } }];\n"
},
"id": "engine",
"name": "Clawd Engine",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
80,
130
],
"notes": "PET_NAME is set at the top of this node's code. Edit it there."
},
{
"parameters": {
"method": "PUT",
"url": "http://<AWTRIX_IP>/api/v1/apps/pushed/clawd",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.payload) }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {}
},
"id": "http-draw",
"name": "Push Draw",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
360,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.sound !== null && $json.sound !== undefined }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
"id": "if-sound",
"name": "Has Sound",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
360,
160
]
},
{
"parameters": {
"method": "POST",
"url": "http://<AWTRIX_IP>/api/v1/sounds/play",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ rtttl: $json.sound }) }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {}
},
"id": "http-sound",
"name": "Play Sound",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
620,
160
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.notify !== null && $json.notify !== undefined }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
"id": "if-notify",
"name": "Has Notify",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
360,
320
]
},
{
"parameters": {
"method": "POST",
"url": "http://<AWTRIX_IP>/api/v1/notifications",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.notify) }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {}
},
"id": "http-notify",
"name": "Send Notify",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
620,
320
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.switchTo === true }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
"id": "if-switchto",
"name": "Has SwitchTo",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
360,
480
]
},
{
"parameters": {
"method": "PUT",
"url": "http://<AWTRIX_IP>/api/v1/apps/active",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ name: 'clawd', fast: true }) }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {}
},
"id": "http-switchto",
"name": "Switch To Clawd",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
620,
480
]
}
],
"connections": {
"Tick every 2s": {
"main": [
[
{
"node": "Normalize Tick",
"type": "main",
"index": 0
}
]
]
},
"Normalize Tick": {
"main": [
[
{
"node": "Clawd Engine",
"type": "main",
"index": 0
}
]
]
},
"Button Events (MQTT)": {
"main": [
[
{
"node": "Normalize Button",
"type": "main",
"index": 0
}
]
]
},
"Normalize Button": {
"main": [
[
{
"node": "Clawd Engine",
"type": "main",
"index": 0
}
]
]
},
"HA Action (Webhook)": {
"main": [
[
{
"node": "Normalize Action",
"type": "main",
"index": 0
}
]
]
},
"Normalize Action": {
"main": [
[
{
"node": "Clawd Engine",
"type": "main",
"index": 0
}
]
]
},
"Clawd Engine": {
"main": [
[
{
"node": "Push Draw",
"type": "main",
"index": 0
},
{
"node": "Has Sound",
"type": "main",
"index": 0
},
{
"node": "Has Notify",
"type": "main",
"index": 0
},
{
"node": "Has SwitchTo",
"type": "main",
"index": 0
}
]
]
},
"Has Sound": {
"main": [
[
{
"node": "Play Sound",
"type": "main",
"index": 0
}
],
[]
]
},
"Has Notify": {
"main": [
[
{
"node": "Send Notify",
"type": "main",
"index": 0
}
],
[]
]
},
"Has SwitchTo": {
"main": [
[
{
"node": "Switch To Clawd",
"type": "main",
"index": 0
}
],
[]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}