matebot 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. matebot-0.2.0/.github/workflows/ci.yml +20 -0
  2. matebot-0.2.0/.github/workflows/docker.yml +35 -0
  3. matebot-0.2.0/.github/workflows/release.yml +20 -0
  4. matebot-0.2.0/.gitignore +8 -0
  5. matebot-0.2.0/AGENTS.md +101 -0
  6. matebot-0.2.0/CLAUDE.md +2 -0
  7. matebot-0.2.0/Dockerfile +19 -0
  8. matebot-0.2.0/LICENSE +21 -0
  9. matebot-0.2.0/PKG-INFO +232 -0
  10. matebot-0.2.0/README.md +198 -0
  11. matebot-0.2.0/docker-compose.example.yml +19 -0
  12. matebot-0.2.0/docs/screenshots/journal-detail.png +0 -0
  13. matebot-0.2.0/docs/screenshots/journal-list.png +0 -0
  14. matebot-0.2.0/docs/screenshots/telegram-chat.png +0 -0
  15. matebot-0.2.0/flake.lock +27 -0
  16. matebot-0.2.0/flake.nix +137 -0
  17. matebot-0.2.0/pyproject.toml +48 -0
  18. matebot-0.2.0/src/matebot/__init__.py +3 -0
  19. matebot-0.2.0/src/matebot/bags.py +77 -0
  20. matebot-0.2.0/src/matebot/cli.py +293 -0
  21. matebot-0.2.0/src/matebot/commands.py +272 -0
  22. matebot-0.2.0/src/matebot/config.py +91 -0
  23. matebot-0.2.0/src/matebot/conversation.py +263 -0
  24. matebot-0.2.0/src/matebot/digest.py +80 -0
  25. matebot-0.2.0/src/matebot/hints.py +53 -0
  26. matebot-0.2.0/src/matebot/machine.py +195 -0
  27. matebot-0.2.0/src/matebot/messengers/__init__.py +28 -0
  28. matebot-0.2.0/src/matebot/messengers/base.py +58 -0
  29. matebot-0.2.0/src/matebot/messengers/discord.py +88 -0
  30. matebot-0.2.0/src/matebot/messengers/telegram.py +118 -0
  31. matebot-0.2.0/src/matebot/plot.py +100 -0
  32. matebot-0.2.0/src/matebot/sitegen.py +119 -0
  33. matebot-0.2.0/src/matebot/slog.py +225 -0
  34. matebot-0.2.0/src/matebot/state.py +42 -0
  35. matebot-0.2.0/src/matebot/sync.py +163 -0
  36. matebot-0.2.0/src/matebot/watcher.py +137 -0
  37. matebot-0.2.0/src/matebot/web/app.js +395 -0
  38. matebot-0.2.0/src/matebot/web/index.html +43 -0
  39. matebot-0.2.0/src/matebot/web/style.css +166 -0
  40. matebot-0.2.0/src/matebot/web/vendor/chart.umd.js +20 -0
  41. matebot-0.2.0/src/matebot/web/vendor/chartjs-plugin-annotation.min.js +7 -0
  42. matebot-0.2.0/tests/fixtures/000004.slog +0 -0
  43. matebot-0.2.0/tests/fixtures/status_frames.jsonl +64 -0
  44. matebot-0.2.0/tests/test_bags.py +47 -0
  45. matebot-0.2.0/tests/test_cleaning.py +39 -0
  46. matebot-0.2.0/tests/test_commands.py +211 -0
  47. matebot-0.2.0/tests/test_conversation.py +170 -0
  48. matebot-0.2.0/tests/test_digest.py +43 -0
  49. matebot-0.2.0/tests/test_hints.py +38 -0
  50. matebot-0.2.0/tests/test_plot.py +17 -0
  51. matebot-0.2.0/tests/test_sitegen.py +42 -0
  52. matebot-0.2.0/tests/test_slog.py +80 -0
  53. matebot-0.2.0/tests/test_sync_retry.py +43 -0
  54. matebot-0.2.0/tests/test_watcher.py +70 -0
@@ -0,0 +1,20 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python: ["3.11", "3.12", "3.13"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python }}
18
+ - run: pip install ".[all,dev]"
19
+ - run: ruff check src tests
20
+ - run: pytest -q
@@ -0,0 +1,35 @@
1
+ name: Docker
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ packages: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: docker/setup-qemu-action@v3
16
+ - uses: docker/setup-buildx-action@v3
17
+ - uses: docker/login-action@v3
18
+ with:
19
+ registry: ghcr.io
20
+ username: ${{ github.actor }}
21
+ password: ${{ secrets.GITHUB_TOKEN }}
22
+ - uses: docker/metadata-action@v5
23
+ id: meta
24
+ with:
25
+ images: ghcr.io/${{ github.repository_owner }}/matebot
26
+ tags: |
27
+ type=raw,value=latest,enable={{is_default_branch}}
28
+ type=semver,pattern={{version}}
29
+ - uses: docker/build-push-action@v6
30
+ with:
31
+ context: .
32
+ platforms: linux/amd64,linux/arm64
33
+ push: true
34
+ tags: ${{ steps.meta.outputs.tags }}
35
+ labels: ${{ steps.meta.outputs.labels }}
@@ -0,0 +1,20 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+
6
+ jobs:
7
+ pypi:
8
+ runs-on: ubuntu-latest
9
+ environment: pypi
10
+ permissions:
11
+ id-token: write # trusted publishing (OIDC), no tokens stored
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.13"
18
+ - run: pip install build
19
+ - run: python -m build
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ result
7
+ dist/
8
+ state.json
@@ -0,0 +1,101 @@
1
+ # Agent instructions for MATEbot
2
+
3
+ You are likely here because a human asked their AI assistant to help with
4
+ MATEbot — either to **set it up** for their GaggiMate espresso machine or to
5
+ **work on the code**. Both playbooks follow.
6
+
7
+ ## What this is
8
+
9
+ MATEbot watches a GaggiMate machine over its local WebSocket API, messages the
10
+ user after every shot (Telegram or Discord) to log rating/beans/grind/doses,
11
+ writes those notes back into the machine's own shot history, and optionally
12
+ archives everything to a git repo with a generated shot-journal site.
13
+
14
+ ## Playbook: helping someone set it up
15
+
16
+ Adapt to the user's skill level — ask what they have before prescribing.
17
+ The decision tree:
18
+
19
+ 1. **Where will it run?** It needs any always-on device on the same network
20
+ as the machine: a Raspberry Pi, NAS, home server, old laptop. No port
21
+ forwarding, no domain — outbound internet only.
22
+ 2. **Pick an install path:**
23
+ - Most users → **Docker**: copy `docker-compose.example.yml`, fill in env
24
+ vars, `docker compose up -d`. Image: `ghcr.io/alexnly/matebot:latest`
25
+ (multi-arch, Raspberry Pi works).
26
+ - Python users → `pip install "matebot[telegram] @ git+https://github.com/AlexNly/MATEbot"`,
27
+ then `matebot run` (systemd unit or `screen` for persistence).
28
+ - NixOS users → flake input + `services.matebot` module (see README).
29
+ 3. **Telegram bot** (the fiddly part for novices — walk them through it):
30
+ 1. In Telegram, talk to `@BotFather` → `/newbot` → pick a name → copy the
31
+ token (looks like `123456:ABC-...`). Never paste tokens into git.
32
+ 2. Have the user send their new bot any message.
33
+ 3. Get the chat id: `https://api.telegram.org/bot<TOKEN>/getUpdates` →
34
+ `result[0].message.chat.id`.
35
+ 4. `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` go into the environment
36
+ (compose file, EnvironmentFile, or shell).
37
+ 4. **Machine address**: set `MATEBOT_MACHINE_HOST`. Prefer the machine's IP
38
+ over `gaggimate.local` (mDNS is flaky on some networks) and recommend a
39
+ static DHCP lease in the router.
40
+ 5. **Verify**: start the bot, run a ≥10 s brew-mode shot (an empty blind
41
+ basket works; water/steam modes do not trigger). The questionnaire should
42
+ arrive within seconds of the shot ending.
43
+ 6. **Optional extras**, in ascending effort: journal repo + GitHub Pages
44
+ (README "Publishing your journal"), dial-in hints (on by default), bean
45
+ bag tracking (`/newbag`), smart plug hooks for cold-start `/wake`
46
+ (README "Smart plug cold start").
47
+
48
+ Common failure modes: token/chat id swapped or quoted wrong; bot and machine
49
+ on different networks/VLANs; machine powered off at a dumb power strip;
50
+ shots shorter than `MATEBOT_MIN_SHOT_S` (default 10 s) being ignored;
51
+ firmware older than v1.7 (no binary shot logs — unsupported).
52
+
53
+ ## Playbook: working on the code
54
+
55
+ - Layout: `src/matebot/` — `machine.py` (WS/HTTP client), `watcher.py` (shot
56
+ detection), `conversation.py` (questionnaire engine, messenger-agnostic),
57
+ `messengers/` (Telegram/Discord backends), `commands.py` (slash commands),
58
+ `hints.py`, `bags.py`, `digest.py` (features), `sync.py` (git archive),
59
+ `sitegen.py` + `web/` (journal site), `plot.py` (Telegram PNG chart),
60
+ `slog.py` (binary format decoder — start here to understand the data).
61
+ - Dev loop: `pip install -e ".[all,dev]"` then `pytest -q` and
62
+ `ruff check src tests`. Or `nix develop`.
63
+ - Contribution flow: branch → PR → CI must pass (branch protection, no direct
64
+ pushes to `main`) → squash merge. Keep commits/PRs single-purpose.
65
+ - Tests are required for behavior changes. The conversation engine is tested
66
+ against a `FakeMessenger`; the watcher against a recorded frame fixture
67
+ (`tests/fixtures/status_frames.jsonl`); the decoder against a real shot
68
+ (`tests/fixtures/000004.slog` — golden values in `test_slog.py`).
69
+
70
+ ### GaggiMate firmware gotchas (hard-won; do not rediscover these)
71
+
72
+ - `req:history:notes:save`: the request-level `id` becomes the notes filename
73
+ verbatim. Older firmware reads the zero-padded name (`000059.json`),
74
+ current nightlies the unpadded one (`59.json`) — MATEbot saves under BOTH.
75
+ `rating` must be a number, every other field a string.
76
+ - `req:change-mode` gets **no response** — never wait for one (fire-and-forget
77
+ via `send_event`; confirmation comes from the status stream).
78
+ - Missing files under `/api/history/` return **HTTP 200 with gzipped SPA
79
+ index.html**, not 404 — validate content (`SHOT` magic / leading `{`) on
80
+ every fetch.
81
+ - The ESP32 drops WebSocket clients whenever its send queue fills (e.g. the
82
+ web UI is open). Reconnect loops must be bulletproof; nothing may
83
+ crash-loop on a dropped socket.
84
+ - New-shot ids come from polling `/api/history/index.bin` (`flags & 1`,
85
+ `id > last_known`) — never from `req:history:list` (heavy on the ESP32)
86
+ and never from timestamps (`startEpoch` is wrong without NTP).
87
+ - Shot files are finalized up to ~1 min after the pump stops (bluetooth
88
+ scale settle time).
89
+
90
+ ### License boundary (important)
91
+
92
+ GaggiMate's own source (including its web UI) is **CC BY-NC-SA** — it must
93
+ never be copied into this MIT repo. Matching its *look* (colors, layout,
94
+ axis ranges) via independent implementation is fine and is what
95
+ `web/app.js` and `plot.py` do.
96
+
97
+ ### Secrets
98
+
99
+ Tokens live in the environment only. `settings.json` written to data repos
100
+ must keep `wifiSsid`, `wifiPassword`, `apPassword`, `haPassword` redacted
101
+ (`machine.py` does this — keep it that way).
@@ -0,0 +1,2 @@
1
+ See [AGENTS.md](AGENTS.md) — setup playbook, dev guide, firmware gotchas,
2
+ and the license boundary all live there.
@@ -0,0 +1,19 @@
1
+ FROM python:3.13-slim
2
+
3
+ # git + ssh are only needed for the optional data-repo sync
4
+ RUN apt-get update \
5
+ && apt-get install -y --no-install-recommends git openssh-client \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ WORKDIR /app
9
+ COPY pyproject.toml README.md LICENSE ./
10
+ COPY src ./src
11
+ RUN pip install --no-cache-dir ".[all]"
12
+
13
+ RUN useradd -m -u 1000 matebot
14
+ USER matebot
15
+ ENV MATEBOT_STATE_DIR=/data
16
+ VOLUME /data
17
+
18
+ ENTRYPOINT ["matebot"]
19
+ CMD ["run"]
matebot-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Nicolay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
matebot-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,232 @@
1
+ Metadata-Version: 2.4
2
+ Name: matebot
3
+ Version: 0.2.0
4
+ Summary: The proactive companion for GaggiMate espresso machines: post-shot logging bot, .slog decoder, shot-journal site generator
5
+ Project-URL: Homepage, https://github.com/AlexNly/matebot
6
+ Project-URL: Issues, https://github.com/AlexNly/matebot/issues
7
+ Author: Alexander Nicolay
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: coffee,discord,espresso,gaggimate,homelab,telegram
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Home Automation
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: aiohttp>=3.9
16
+ Provides-Extra: all
17
+ Requires-Dist: discord-py>=2.3; extra == 'all'
18
+ Requires-Dist: matplotlib>=3.8; extra == 'all'
19
+ Requires-Dist: matrix-nio>=0.24; extra == 'all'
20
+ Requires-Dist: python-telegram-bot<23,>=21; extra == 'all'
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Requires-Dist: ruff>=0.4; extra == 'dev'
25
+ Provides-Extra: discord
26
+ Requires-Dist: discord-py>=2.3; extra == 'discord'
27
+ Provides-Extra: matrix
28
+ Requires-Dist: matrix-nio>=0.24; extra == 'matrix'
29
+ Provides-Extra: plots
30
+ Requires-Dist: matplotlib>=3.8; extra == 'plots'
31
+ Provides-Extra: telegram
32
+ Requires-Dist: python-telegram-bot<23,>=21; extra == 'telegram'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # matebot
36
+
37
+ A post-shot companion for [GaggiMate](https://gaggimate.eu) espresso machines.
38
+
39
+ I kept forgetting to log my shots. Not for lack of caring — but after every
40
+ shot the ritual was: pull out the phone, open the web UI, find Shot History,
41
+ tap edit, type everything in. Most days that didn't happen, and by 23:47,
42
+ lying in bed, the grind setting of today's best shot was gone for good.
43
+
44
+ matebot turns the workflow around: when a shot finishes, the machine messages
45
+ *you* and asks the few things you'd otherwise forget — rating, taste, beans,
46
+ grind, doses. Thirty seconds of tapping while you sip. The answers are written
47
+ straight back into GaggiMate's own Shot Notes, exactly as if you'd typed them
48
+ into the web UI.
49
+
50
+ <p align="center">
51
+ <img src="docs/screenshots/telegram-chat.png" width="420" alt="matebot asking for a shot rating on Telegram right after the shot finished">
52
+ </p>
53
+
54
+ ## What it does
55
+
56
+ - Watches the machine over its WebSocket API and detects finished brew shots
57
+ (backflush/descale/flush runs and anything under 10 s are ignored).
58
+ - Runs a short questionnaire via **Telegram** or **Discord** (Matrix planned),
59
+ with one-tap "same as last shot" defaults for beans, grind and dose.
60
+ - Saves the answers into the machine's shot history — GaggiMate stays the
61
+ source of truth, with or without matebot.
62
+ - Optionally archives every shot (`.slog` + notes), your brew profiles and
63
+ machine settings (credentials redacted) to a git repository after each shot.
64
+ - Generates a static **shot journal** from that archive, ready for GitHub
65
+ Pages. Because the journal lives outside the machine, it survives firmware
66
+ updates and downgrades, a dying SD card, or a water-damaged machine.
67
+ - Ships a standalone `.slog` decoder (`matebot decode shot.slog --csv`).
68
+ - After a sour, bitter or low-rated shot it suggests the next dial-in step
69
+ (grind → ratio → temperature, one variable at a time), following
70
+ [modsmthng's Automatic Pro cheat sheet](https://modsmthng.github.io/Automatic-Pro/)
71
+ — disable with `MATEBOT_HINTS=0`.
72
+
73
+ ## The shot journal
74
+
75
+ [Live example](https://alexnly.github.io/GAGGIMATE-0614/) — every shot with
76
+ the familiar combined pressure/flow/temperature chart, phase markers, ratings
77
+ and notes.
78
+
79
+ <p align="center">
80
+ <img src="docs/screenshots/journal-list.png" width="49%" alt="Shot journal list with ratings, ratios and peak pressure">
81
+ <img src="docs/screenshots/journal-detail.png" width="49%" alt="Per-shot detail with combined pressure/flow/temperature chart">
82
+ </p>
83
+
84
+ ## Install
85
+
86
+ ### Docker
87
+
88
+ ```bash
89
+ mkdir matebot && cd matebot
90
+ curl -O https://raw.githubusercontent.com/AlexNly/matebot/main/docker-compose.example.yml
91
+ cp docker-compose.example.yml docker-compose.yml
92
+ # edit: machine host, bot token, chat id
93
+ docker compose up -d
94
+ ```
95
+
96
+ Images are multi-arch (`amd64` + `arm64`, so a Raspberry Pi works):
97
+ `ghcr.io/alexnly/matebot:latest`. There is no cloud service behind this —
98
+ the bot needs to run on something in your home network that is always on.
99
+ A Pi Zero 2 W and a USB charger is the whole data center.
100
+
101
+ ### pip
102
+
103
+ ```bash
104
+ pip install "matebot[telegram] @ git+https://github.com/AlexNly/matebot"
105
+ matebot run
106
+ ```
107
+
108
+ ### NixOS (flake)
109
+
110
+ ```nix
111
+ inputs.matebot.url = "github:AlexNly/matebot";
112
+
113
+ services.matebot = {
114
+ enable = true;
115
+ machineHost = "192.168.1.50";
116
+ environmentFile = "/etc/secrets/matebot"; # TELEGRAM_BOT_TOKEN=... / TELEGRAM_CHAT_ID=...
117
+ dataRepo = "/var/lib/gaggimate-journal"; # optional
118
+ };
119
+ ```
120
+
121
+ ## Setup
122
+
123
+ 1. Telegram: create a bot with [@BotFather](https://t.me/BotFather)
124
+ (`/newbot`), copy the token. Message your bot once, then read your chat id
125
+ from `https://api.telegram.org/bot<TOKEN>/getUpdates`.
126
+ Discord: create an application + bot, invite it to a server, enable the
127
+ *message content* intent, copy the channel id.
128
+ 2. Point `MATEBOT_MACHINE_HOST` at your GaggiMate. A DHCP reservation is more
129
+ reliable than `gaggimate.local`.
130
+ 3. Pull a shot.
131
+
132
+ ## Chat commands
133
+
134
+ Besides the post-shot questionnaire, the bot answers commands (any messenger):
135
+
136
+ ```
137
+ /wake turn the machine on — pings you when it's at temperature
138
+ /sleep back to standby
139
+ /status mode, boiler temperature, water level
140
+ /last the last logged shot (with journal link if configured)
141
+ /fix redo the questionnaire for the last shot
142
+ /help list commands
143
+ ```
144
+
145
+ ## Smart plug cold start (optional)
146
+
147
+ GaggiMate in standby still draws power, so many people cut it at a smart
148
+ plug — which normally kills `/wake`. Give MATEbot the plug's on/off commands
149
+ and `/wake` becomes a true cold start (plug on → wait for the machine to
150
+ boot → brew mode → ready ping), while `/sleep` powers everything down:
151
+
152
+ ```bash
153
+ # Tasmota (Nous A1T, Eightree, ...)
154
+ MATEBOT_WAKE_HOOK='curl -sf "http://192.168.1.60/cm?cmnd=Power%20On"'
155
+ MATEBOT_SLEEP_HOOK='curl -sf "http://192.168.1.60/cm?cmnd=Power%20Off"'
156
+
157
+ # Shelly
158
+ MATEBOT_WAKE_HOOK='curl -sf "http://192.168.1.60/relay/0?turn=on"'
159
+ ```
160
+
161
+ Any shell command works (Home Assistant webhook, `tinytuya`, zigbee2mqtt…).
162
+
163
+ ## Configuration
164
+
165
+ Environment variables, or the same keys in `~/.config/matebot/config.toml`:
166
+
167
+ | Variable | Default | Meaning |
168
+ |---|---|---|
169
+ | `MATEBOT_MACHINE_HOST` | `gaggimate.local` | GaggiMate hostname/IP |
170
+ | `MATEBOT_MESSENGER` | `telegram` | `telegram` or `discord` |
171
+ | `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID` | — | Telegram credentials |
172
+ | `DISCORD_BOT_TOKEN` / `DISCORD_CHANNEL_ID` | — | Discord credentials |
173
+ | `MATEBOT_DATA_REPO` | — | Path to a git clone; enables archive + journal |
174
+ | `MATEBOT_SITE_TITLE` | `Shot Journal` | Title of the generated journal |
175
+ | `MATEBOT_JOURNAL_URL` | — | Public journal URL, used for `/last` deep links |
176
+ | `MATEBOT_HINTS` | `1` | Dial-in hints after sour/bitter/low-rated shots |
177
+ | `MATEBOT_STATE_DIR` | `~/.local/state/matebot` | Bot state (defaults, resume) |
178
+ | `MATEBOT_MIN_SHOT_S` | `10` | Ignore shots shorter than this |
179
+ | `MATEBOT_IGNORE_PROFILES` | `(?i)backflush\|descale\|flush\|clean` | Profile regex to skip |
180
+
181
+ ## CLI
182
+
183
+ ```
184
+ matebot run # the bot (--replay frames.jsonl --dry-run to test)
185
+ matebot decode SHOT.slog # .slog -> JSON (--csv for CSV)
186
+ matebot sitegen shots/ -o docs/ --title "My Shot Journal"
187
+ matebot sync # one-off journal sync (shots, profiles, settings, site)
188
+ ```
189
+
190
+ ## Publishing your journal on GitHub Pages
191
+
192
+ 1. Create a repo for your data, clone it where matebot runs, set
193
+ `MATEBOT_DATA_REPO` to the clone.
194
+ 2. On GitHub: Settings → Pages → Deploy from branch → `main` / `/docs`.
195
+ 3. Every shot now updates `https://<you>.github.io/<repo>/`.
196
+
197
+ ## How it talks to the machine
198
+
199
+ Local network only — nothing leaves your LAN except the messenger API and
200
+ your own git remote. Requires GaggiMate firmware ≥ v1.7 (binary shot logs).
201
+
202
+ - `ws://<machine>/ws` — `evt:status` for shot detection,
203
+ `req:history:notes:save` for notes, `req:profiles:list` for backup
204
+ - `GET /api/history/index.bin`, `<id>.slog`, `<id>.json` — shot downloads
205
+ - `GET /api/settings` — settings backup; WiFi/AP/Home-Assistant credentials
206
+ are redacted before anything is written to disk
207
+
208
+ The `.slog` v5 binary format (512-byte header, 26-byte samples at 250 ms) is
209
+ documented in [`src/matebot/slog.py`](src/matebot/slog.py).
210
+
211
+ ### WhatsApp?
212
+
213
+ There is no reasonable self-hosted WhatsApp bot API. Two workable paths:
214
+ bridge your Telegram/Matrix chat via [mautrix](https://docs.mau.fi/bridges/),
215
+ or Meta's WhatsApp Business Cloud API (requires a business account). Native
216
+ support: contributions welcome.
217
+
218
+ ## Credits
219
+
220
+ - [GaggiMate](https://gaggimate.eu) by jniebuhr — the machine controller this
221
+ companion talks to.
222
+ - The journal's shot chart recreates the look of GaggiMate's own web UI
223
+ (independent reimplementation — GaggiMate's UI code is CC BY-NC-SA and none
224
+ of it is copied). Rendered with [Chart.js](https://www.chartjs.org/) and
225
+ chartjs-plugin-annotation (MIT, vendored).
226
+ - The dial-in hints follow the guide from
227
+ [Automatic Pro cheat sheet](https://modsmthng.github.io/Automatic-Pro/) by
228
+ **modsmthng**.
229
+
230
+ ## License
231
+
232
+ MIT. Not affiliated with the GaggiMate project.
@@ -0,0 +1,198 @@
1
+ # matebot
2
+
3
+ A post-shot companion for [GaggiMate](https://gaggimate.eu) espresso machines.
4
+
5
+ I kept forgetting to log my shots. Not for lack of caring — but after every
6
+ shot the ritual was: pull out the phone, open the web UI, find Shot History,
7
+ tap edit, type everything in. Most days that didn't happen, and by 23:47,
8
+ lying in bed, the grind setting of today's best shot was gone for good.
9
+
10
+ matebot turns the workflow around: when a shot finishes, the machine messages
11
+ *you* and asks the few things you'd otherwise forget — rating, taste, beans,
12
+ grind, doses. Thirty seconds of tapping while you sip. The answers are written
13
+ straight back into GaggiMate's own Shot Notes, exactly as if you'd typed them
14
+ into the web UI.
15
+
16
+ <p align="center">
17
+ <img src="docs/screenshots/telegram-chat.png" width="420" alt="matebot asking for a shot rating on Telegram right after the shot finished">
18
+ </p>
19
+
20
+ ## What it does
21
+
22
+ - Watches the machine over its WebSocket API and detects finished brew shots
23
+ (backflush/descale/flush runs and anything under 10 s are ignored).
24
+ - Runs a short questionnaire via **Telegram** or **Discord** (Matrix planned),
25
+ with one-tap "same as last shot" defaults for beans, grind and dose.
26
+ - Saves the answers into the machine's shot history — GaggiMate stays the
27
+ source of truth, with or without matebot.
28
+ - Optionally archives every shot (`.slog` + notes), your brew profiles and
29
+ machine settings (credentials redacted) to a git repository after each shot.
30
+ - Generates a static **shot journal** from that archive, ready for GitHub
31
+ Pages. Because the journal lives outside the machine, it survives firmware
32
+ updates and downgrades, a dying SD card, or a water-damaged machine.
33
+ - Ships a standalone `.slog` decoder (`matebot decode shot.slog --csv`).
34
+ - After a sour, bitter or low-rated shot it suggests the next dial-in step
35
+ (grind → ratio → temperature, one variable at a time), following
36
+ [modsmthng's Automatic Pro cheat sheet](https://modsmthng.github.io/Automatic-Pro/)
37
+ — disable with `MATEBOT_HINTS=0`.
38
+
39
+ ## The shot journal
40
+
41
+ [Live example](https://alexnly.github.io/GAGGIMATE-0614/) — every shot with
42
+ the familiar combined pressure/flow/temperature chart, phase markers, ratings
43
+ and notes.
44
+
45
+ <p align="center">
46
+ <img src="docs/screenshots/journal-list.png" width="49%" alt="Shot journal list with ratings, ratios and peak pressure">
47
+ <img src="docs/screenshots/journal-detail.png" width="49%" alt="Per-shot detail with combined pressure/flow/temperature chart">
48
+ </p>
49
+
50
+ ## Install
51
+
52
+ ### Docker
53
+
54
+ ```bash
55
+ mkdir matebot && cd matebot
56
+ curl -O https://raw.githubusercontent.com/AlexNly/matebot/main/docker-compose.example.yml
57
+ cp docker-compose.example.yml docker-compose.yml
58
+ # edit: machine host, bot token, chat id
59
+ docker compose up -d
60
+ ```
61
+
62
+ Images are multi-arch (`amd64` + `arm64`, so a Raspberry Pi works):
63
+ `ghcr.io/alexnly/matebot:latest`. There is no cloud service behind this —
64
+ the bot needs to run on something in your home network that is always on.
65
+ A Pi Zero 2 W and a USB charger is the whole data center.
66
+
67
+ ### pip
68
+
69
+ ```bash
70
+ pip install "matebot[telegram] @ git+https://github.com/AlexNly/matebot"
71
+ matebot run
72
+ ```
73
+
74
+ ### NixOS (flake)
75
+
76
+ ```nix
77
+ inputs.matebot.url = "github:AlexNly/matebot";
78
+
79
+ services.matebot = {
80
+ enable = true;
81
+ machineHost = "192.168.1.50";
82
+ environmentFile = "/etc/secrets/matebot"; # TELEGRAM_BOT_TOKEN=... / TELEGRAM_CHAT_ID=...
83
+ dataRepo = "/var/lib/gaggimate-journal"; # optional
84
+ };
85
+ ```
86
+
87
+ ## Setup
88
+
89
+ 1. Telegram: create a bot with [@BotFather](https://t.me/BotFather)
90
+ (`/newbot`), copy the token. Message your bot once, then read your chat id
91
+ from `https://api.telegram.org/bot<TOKEN>/getUpdates`.
92
+ Discord: create an application + bot, invite it to a server, enable the
93
+ *message content* intent, copy the channel id.
94
+ 2. Point `MATEBOT_MACHINE_HOST` at your GaggiMate. A DHCP reservation is more
95
+ reliable than `gaggimate.local`.
96
+ 3. Pull a shot.
97
+
98
+ ## Chat commands
99
+
100
+ Besides the post-shot questionnaire, the bot answers commands (any messenger):
101
+
102
+ ```
103
+ /wake turn the machine on — pings you when it's at temperature
104
+ /sleep back to standby
105
+ /status mode, boiler temperature, water level
106
+ /last the last logged shot (with journal link if configured)
107
+ /fix redo the questionnaire for the last shot
108
+ /help list commands
109
+ ```
110
+
111
+ ## Smart plug cold start (optional)
112
+
113
+ GaggiMate in standby still draws power, so many people cut it at a smart
114
+ plug — which normally kills `/wake`. Give MATEbot the plug's on/off commands
115
+ and `/wake` becomes a true cold start (plug on → wait for the machine to
116
+ boot → brew mode → ready ping), while `/sleep` powers everything down:
117
+
118
+ ```bash
119
+ # Tasmota (Nous A1T, Eightree, ...)
120
+ MATEBOT_WAKE_HOOK='curl -sf "http://192.168.1.60/cm?cmnd=Power%20On"'
121
+ MATEBOT_SLEEP_HOOK='curl -sf "http://192.168.1.60/cm?cmnd=Power%20Off"'
122
+
123
+ # Shelly
124
+ MATEBOT_WAKE_HOOK='curl -sf "http://192.168.1.60/relay/0?turn=on"'
125
+ ```
126
+
127
+ Any shell command works (Home Assistant webhook, `tinytuya`, zigbee2mqtt…).
128
+
129
+ ## Configuration
130
+
131
+ Environment variables, or the same keys in `~/.config/matebot/config.toml`:
132
+
133
+ | Variable | Default | Meaning |
134
+ |---|---|---|
135
+ | `MATEBOT_MACHINE_HOST` | `gaggimate.local` | GaggiMate hostname/IP |
136
+ | `MATEBOT_MESSENGER` | `telegram` | `telegram` or `discord` |
137
+ | `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID` | — | Telegram credentials |
138
+ | `DISCORD_BOT_TOKEN` / `DISCORD_CHANNEL_ID` | — | Discord credentials |
139
+ | `MATEBOT_DATA_REPO` | — | Path to a git clone; enables archive + journal |
140
+ | `MATEBOT_SITE_TITLE` | `Shot Journal` | Title of the generated journal |
141
+ | `MATEBOT_JOURNAL_URL` | — | Public journal URL, used for `/last` deep links |
142
+ | `MATEBOT_HINTS` | `1` | Dial-in hints after sour/bitter/low-rated shots |
143
+ | `MATEBOT_STATE_DIR` | `~/.local/state/matebot` | Bot state (defaults, resume) |
144
+ | `MATEBOT_MIN_SHOT_S` | `10` | Ignore shots shorter than this |
145
+ | `MATEBOT_IGNORE_PROFILES` | `(?i)backflush\|descale\|flush\|clean` | Profile regex to skip |
146
+
147
+ ## CLI
148
+
149
+ ```
150
+ matebot run # the bot (--replay frames.jsonl --dry-run to test)
151
+ matebot decode SHOT.slog # .slog -> JSON (--csv for CSV)
152
+ matebot sitegen shots/ -o docs/ --title "My Shot Journal"
153
+ matebot sync # one-off journal sync (shots, profiles, settings, site)
154
+ ```
155
+
156
+ ## Publishing your journal on GitHub Pages
157
+
158
+ 1. Create a repo for your data, clone it where matebot runs, set
159
+ `MATEBOT_DATA_REPO` to the clone.
160
+ 2. On GitHub: Settings → Pages → Deploy from branch → `main` / `/docs`.
161
+ 3. Every shot now updates `https://<you>.github.io/<repo>/`.
162
+
163
+ ## How it talks to the machine
164
+
165
+ Local network only — nothing leaves your LAN except the messenger API and
166
+ your own git remote. Requires GaggiMate firmware ≥ v1.7 (binary shot logs).
167
+
168
+ - `ws://<machine>/ws` — `evt:status` for shot detection,
169
+ `req:history:notes:save` for notes, `req:profiles:list` for backup
170
+ - `GET /api/history/index.bin`, `<id>.slog`, `<id>.json` — shot downloads
171
+ - `GET /api/settings` — settings backup; WiFi/AP/Home-Assistant credentials
172
+ are redacted before anything is written to disk
173
+
174
+ The `.slog` v5 binary format (512-byte header, 26-byte samples at 250 ms) is
175
+ documented in [`src/matebot/slog.py`](src/matebot/slog.py).
176
+
177
+ ### WhatsApp?
178
+
179
+ There is no reasonable self-hosted WhatsApp bot API. Two workable paths:
180
+ bridge your Telegram/Matrix chat via [mautrix](https://docs.mau.fi/bridges/),
181
+ or Meta's WhatsApp Business Cloud API (requires a business account). Native
182
+ support: contributions welcome.
183
+
184
+ ## Credits
185
+
186
+ - [GaggiMate](https://gaggimate.eu) by jniebuhr — the machine controller this
187
+ companion talks to.
188
+ - The journal's shot chart recreates the look of GaggiMate's own web UI
189
+ (independent reimplementation — GaggiMate's UI code is CC BY-NC-SA and none
190
+ of it is copied). Rendered with [Chart.js](https://www.chartjs.org/) and
191
+ chartjs-plugin-annotation (MIT, vendored).
192
+ - The dial-in hints follow the guide from
193
+ [Automatic Pro cheat sheet](https://modsmthng.github.io/Automatic-Pro/) by
194
+ **modsmthng**.
195
+
196
+ ## License
197
+
198
+ MIT. Not affiliated with the GaggiMate project.