Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository_owner }}/gaggibot
images: ghcr.io/${{ github.repository_owner }}/matebot
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ COPY pyproject.toml README.md LICENSE ./
COPY src ./src
RUN pip install --no-cache-dir ".[all]"

RUN useradd -m -u 1000 gaggibot
USER gaggibot
ENV GAGGIBOT_STATE_DIR=/data
RUN useradd -m -u 1000 matebot
USER matebot
ENV MATEBOT_STATE_DIR=/data
VOLUME /data

ENTRYPOINT ["gaggibot"]
ENTRYPOINT ["matebot"]
CMD ["run"]
64 changes: 32 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# gaggibot
# matebot

A post-shot companion for [GaggiMate](https://gaggimate.eu) espresso machines.

Expand All @@ -7,14 +7,14 @@ shot the ritual was: pull out the phone, open the web UI, find Shot History,
tap edit, type everything in. Most days that didn't happen, and by 23:47,
lying in bed, the grind setting of today's best shot was gone for good.

gaggibot turns the workflow around: when a shot finishes, the machine messages
matebot turns the workflow around: when a shot finishes, the machine messages
*you* and asks the few things you'd otherwise forget — rating, taste, beans,
grind, doses. Thirty seconds of tapping while you sip. The answers are written
straight back into GaggiMate's own Shot Notes, exactly as if you'd typed them
into the web UI.

<p align="center">
<img src="docs/screenshots/telegram-chat.png" width="420" alt="gaggibot asking for a shot rating on Telegram right after the shot finished">
<img src="docs/screenshots/telegram-chat.png" width="420" alt="matebot asking for a shot rating on Telegram right after the shot finished">
</p>

## What it does
Expand All @@ -24,17 +24,17 @@ into the web UI.
- Runs a short questionnaire via **Telegram** or **Discord** (Matrix planned),
with one-tap "same as last shot" defaults for beans, grind and dose.
- Saves the answers into the machine's shot history — GaggiMate stays the
source of truth, with or without gaggibot.
source of truth, with or without matebot.
- Optionally archives every shot (`.slog` + notes), your brew profiles and
machine settings (credentials redacted) to a git repository after each shot.
- Generates a static **shot journal** from that archive, ready for GitHub
Pages. Because the journal lives outside the machine, it survives firmware
updates and downgrades, a dying SD card, or a water-damaged machine.
- Ships a standalone `.slog` decoder (`gaggibot decode shot.slog --csv`).
- Ships a standalone `.slog` decoder (`matebot decode shot.slog --csv`).
- After a sour, bitter or low-rated shot it suggests the next dial-in step
(grind → ratio → temperature, one variable at a time), following
[modsmthng's Automatic Pro cheat sheet](https://modsmthng.github.io/Automatic-Pro/)
— disable with `GAGGIBOT_HINTS=0`.
— disable with `MATEBOT_HINTS=0`.

## The shot journal

Expand All @@ -52,34 +52,34 @@ and notes.
### Docker

```bash
mkdir gaggibot && cd gaggibot
curl -O https://raw.githubusercontent.com/AlexNly/gaggibot/main/docker-compose.example.yml
mkdir matebot && cd matebot
curl -O https://raw.githubusercontent.com/AlexNly/matebot/main/docker-compose.example.yml
cp docker-compose.example.yml docker-compose.yml
# edit: machine host, bot token, chat id
docker compose up -d
```

Images are multi-arch (`amd64` + `arm64`, so a Raspberry Pi works):
`ghcr.io/alexnly/gaggibot:latest`. There is no cloud service behind this —
`ghcr.io/alexnly/matebot:latest`. There is no cloud service behind this —
the bot needs to run on something in your home network that is always on.
A Pi Zero 2 W and a USB charger is the whole data center.

### pip

```bash
pip install "gaggibot[telegram] @ git+https://github.com/AlexNly/gaggibot"
gaggibot run
pip install "matebot[telegram] @ git+https://github.com/AlexNly/matebot"
matebot run
```

### NixOS (flake)

```nix
inputs.gaggibot.url = "github:AlexNly/gaggibot";
inputs.matebot.url = "github:AlexNly/matebot";

services.gaggibot = {
services.matebot = {
enable = true;
machineHost = "192.168.1.50";
environmentFile = "/etc/secrets/gaggibot"; # TELEGRAM_BOT_TOKEN=... / TELEGRAM_CHAT_ID=...
environmentFile = "/etc/secrets/matebot"; # TELEGRAM_BOT_TOKEN=... / TELEGRAM_CHAT_ID=...
dataRepo = "/var/lib/gaggimate-journal"; # optional
};
```
Expand All @@ -91,7 +91,7 @@ services.gaggibot = {
from `https://api.telegram.org/bot<TOKEN>/getUpdates`.
Discord: create an application + bot, invite it to a server, enable the
*message content* intent, copy the channel id.
2. Point `GAGGIBOT_MACHINE_HOST` at your GaggiMate. A DHCP reservation is more
2. Point `MATEBOT_MACHINE_HOST` at your GaggiMate. A DHCP reservation is more
reliable than `gaggimate.local`.
3. Pull a shot.

Expand All @@ -110,35 +110,35 @@ Besides the post-shot questionnaire, the bot answers commands (any messenger):

## Configuration

Environment variables, or the same keys in `~/.config/gaggibot/config.toml`:
Environment variables, or the same keys in `~/.config/matebot/config.toml`:

| Variable | Default | Meaning |
|---|---|---|
| `GAGGIBOT_MACHINE_HOST` | `gaggimate.local` | GaggiMate hostname/IP |
| `GAGGIBOT_MESSENGER` | `telegram` | `telegram` or `discord` |
| `MATEBOT_MACHINE_HOST` | `gaggimate.local` | GaggiMate hostname/IP |
| `MATEBOT_MESSENGER` | `telegram` | `telegram` or `discord` |
| `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID` | — | Telegram credentials |
| `DISCORD_BOT_TOKEN` / `DISCORD_CHANNEL_ID` | — | Discord credentials |
| `GAGGIBOT_DATA_REPO` | — | Path to a git clone; enables archive + journal |
| `GAGGIBOT_SITE_TITLE` | `Shot Journal` | Title of the generated journal |
| `GAGGIBOT_JOURNAL_URL` | — | Public journal URL, used for `/last` deep links |
| `GAGGIBOT_HINTS` | `1` | Dial-in hints after sour/bitter/low-rated shots |
| `GAGGIBOT_STATE_DIR` | `~/.local/state/gaggibot` | Bot state (defaults, resume) |
| `GAGGIBOT_MIN_SHOT_S` | `10` | Ignore shots shorter than this |
| `GAGGIBOT_IGNORE_PROFILES` | `(?i)backflush\|descale\|flush\|clean` | Profile regex to skip |
| `MATEBOT_DATA_REPO` | — | Path to a git clone; enables archive + journal |
| `MATEBOT_SITE_TITLE` | `Shot Journal` | Title of the generated journal |
| `MATEBOT_JOURNAL_URL` | — | Public journal URL, used for `/last` deep links |
| `MATEBOT_HINTS` | `1` | Dial-in hints after sour/bitter/low-rated shots |
| `MATEBOT_STATE_DIR` | `~/.local/state/matebot` | Bot state (defaults, resume) |
| `MATEBOT_MIN_SHOT_S` | `10` | Ignore shots shorter than this |
| `MATEBOT_IGNORE_PROFILES` | `(?i)backflush\|descale\|flush\|clean` | Profile regex to skip |

## CLI

```
gaggibot run # the bot (--replay frames.jsonl --dry-run to test)
gaggibot decode SHOT.slog # .slog -> JSON (--csv for CSV)
gaggibot sitegen shots/ -o docs/ --title "My Shot Journal"
gaggibot sync # one-off journal sync (shots, profiles, settings, site)
matebot run # the bot (--replay frames.jsonl --dry-run to test)
matebot decode SHOT.slog # .slog -> JSON (--csv for CSV)
matebot sitegen shots/ -o docs/ --title "My Shot Journal"
matebot sync # one-off journal sync (shots, profiles, settings, site)
```

## Publishing your journal on GitHub Pages

1. Create a repo for your data, clone it where gaggibot runs, set
`GAGGIBOT_DATA_REPO` to the clone.
1. Create a repo for your data, clone it where matebot runs, set
`MATEBOT_DATA_REPO` to the clone.
2. On GitHub: Settings → Pages → Deploy from branch → `main` / `/docs`.
3. Every shot now updates `https://<you>.github.io/<repo>/`.

Expand All @@ -154,7 +154,7 @@ your own git remote. Requires GaggiMate firmware ≥ v1.7 (binary shot logs).
are redacted before anything is written to disk

The `.slog` v5 binary format (512-byte header, 26-byte samples at 250 ms) is
documented in [`src/gaggibot/slog.py`](src/gaggibot/slog.py).
documented in [`src/matebot/slog.py`](src/matebot/slog.py).

### WhatsApp?

Expand Down
14 changes: 7 additions & 7 deletions docker-compose.example.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# gaggibot — minimal deployment.
# matebot — minimal deployment.
# 1. copy to docker-compose.yml 2. fill in the env vars 3. docker compose up -d
services:
gaggibot:
image: ghcr.io/alexnly/gaggibot:latest
matebot:
image: ghcr.io/alexnly/matebot:latest
restart: unless-stopped
environment:
GAGGIBOT_MACHINE_HOST: gaggimate.local # or the machine's IP (recommended)
GAGGIBOT_MESSENGER: telegram # telegram | discord
MATEBOT_MACHINE_HOST: gaggimate.local # or the machine's IP (recommended)
MATEBOT_MESSENGER: telegram # telegram | discord
TELEGRAM_BOT_TOKEN: "123456:ABC..." # from @BotFather
TELEGRAM_CHAT_ID: "12345678" # your chat with the bot
# DISCORD_BOT_TOKEN: "..."
# DISCORD_CHANNEL_ID: "..."
# --- optional: git-backed shot journal + GitHub Pages site ---
# GAGGIBOT_DATA_REPO: /journal
# MATEBOT_DATA_REPO: /journal
volumes:
- ./data:/data # bot state (last shot, defaults)
# - ./journal:/journal # your data repo (git clone)
# - ~/.ssh:/home/gaggibot/.ssh:ro # ssh key for git push
# - ~/.ssh:/home/matebot/.ssh:ro # ssh key for git push
48 changes: 24 additions & 24 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
description = "gaggibot — the proactive companion for GaggiMate espresso machines";
description = "matebot — the proactive companion for GaggiMate espresso machines";

inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";

Expand All @@ -10,9 +10,9 @@
{
packages = forAllSystems (system:
let pkgs = nixpkgs.legacyPackages.${system}; in rec {
default = gaggibot;
gaggibot = pkgs.python3Packages.buildPythonApplication {
pname = "gaggibot";
default = matebot;
matebot = pkgs.python3Packages.buildPythonApplication {
pname = "matebot";
version = "0.1.0";
pyproject = true;
src = self;
Expand All @@ -23,18 +23,18 @@
# discord.py is optional at runtime; add it when using the discord messenger
] ++ pkgs.lib.optional (pkgs.python3Packages ? discordpy) pkgs.python3Packages.discordpy;
nativeCheckInputs = with pkgs.python3Packages; [ pytestCheckHook pytest-asyncio ];
pythonImportsCheck = [ "gaggibot" ];
pythonImportsCheck = [ "matebot" ];
};
});

nixosModules.default = { config, lib, pkgs, ... }:
let
cfg = config.services.gaggibot;
cfg = config.services.matebot;
pkg = self.packages.${pkgs.system}.default;
in
{
options.services.gaggibot = {
enable = lib.mkEnableOption "gaggibot, the GaggiMate shot companion";
options.services.matebot = {
enable = lib.mkEnableOption "matebot, the GaggiMate shot companion";
machineHost = lib.mkOption {
type = lib.types.str;
description = "Hostname/IP of the GaggiMate controller.";
Expand All @@ -54,12 +54,12 @@
};
user = lib.mkOption {
type = lib.types.str;
default = "gaggibot";
default = "matebot";
description = "User to run as (needs git+ssh identity when dataRepo is set).";
};
stateDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/gaggibot";
default = "/var/lib/matebot";
};
minShotDuration = lib.mkOption { type = lib.types.int; default = 10; };
ignoreProfiles = lib.mkOption {
Expand All @@ -69,32 +69,32 @@
};

config = lib.mkIf cfg.enable {
users.users = lib.mkIf (cfg.user == "gaggibot") {
gaggibot = { isSystemUser = true; group = "gaggibot"; home = cfg.stateDir; };
users.users = lib.mkIf (cfg.user == "matebot") {
matebot = { isSystemUser = true; group = "matebot"; home = cfg.stateDir; };
};
users.groups = lib.mkIf (cfg.user == "gaggibot") { gaggibot = { }; };
users.groups = lib.mkIf (cfg.user == "matebot") { matebot = { }; };

systemd.services.gaggibot = {
description = "gaggibot — GaggiMate shot companion";
systemd.services.matebot = {
description = "matebot — GaggiMate shot companion";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path = [ pkgs.git pkgs.openssh ];
environment = {
GAGGIBOT_MACHINE_HOST = cfg.machineHost;
GAGGIBOT_MESSENGER = cfg.messenger;
GAGGIBOT_STATE_DIR = cfg.stateDir;
GAGGIBOT_MIN_SHOT_S = toString cfg.minShotDuration;
GAGGIBOT_IGNORE_PROFILES = cfg.ignoreProfiles;
MATEBOT_MACHINE_HOST = cfg.machineHost;
MATEBOT_MESSENGER = cfg.messenger;
MATEBOT_STATE_DIR = cfg.stateDir;
MATEBOT_MIN_SHOT_S = toString cfg.minShotDuration;
MATEBOT_IGNORE_PROFILES = cfg.ignoreProfiles;
} // lib.optionalAttrs (cfg.dataRepo != null) {
GAGGIBOT_DATA_REPO = toString cfg.dataRepo;
GAGGIBOT_SYNC = "1";
MATEBOT_DATA_REPO = toString cfg.dataRepo;
MATEBOT_SYNC = "1";
};
serviceConfig = {
ExecStart = "${pkg}/bin/gaggibot run";
ExecStart = "${pkg}/bin/matebot run";
EnvironmentFile = cfg.environmentFile;
User = cfg.user;
StateDirectory = lib.mkIf (cfg.stateDir == "/var/lib/gaggibot") "gaggibot";
StateDirectory = lib.mkIf (cfg.stateDir == "/var/lib/matebot") "matebot";
Restart = "always";
RestartSec = "10";
NoNewPrivileges = true;
Expand Down
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "gaggibot"
name = "matebot"
version = "0.1.0"
description = "The proactive companion for GaggiMate espresso machines: post-shot logging bot, .slog decoder, shot-journal site generator"
readme = "README.md"
Expand All @@ -22,18 +22,18 @@ dependencies = ["aiohttp>=3.9"]
telegram = ["python-telegram-bot>=21,<23"]
discord = ["discord.py>=2.3"]
matrix = ["matrix-nio>=0.24"]
all = ["gaggibot[telegram,discord,matrix]"]
all = ["matebot[telegram,discord,matrix]"]
dev = ["pytest>=8", "pytest-asyncio>=0.23", "ruff>=0.4"]

[project.urls]
Homepage = "https://github.com/AlexNly/gaggibot"
Issues = "https://github.com/AlexNly/gaggibot/issues"
Homepage = "https://github.com/AlexNly/matebot"
Issues = "https://github.com/AlexNly/matebot/issues"

[project.scripts]
gaggibot = "gaggibot.cli:main"
matebot = "matebot.cli:main"

[tool.hatch.build.targets.wheel]
packages = ["src/gaggibot"]
packages = ["src/matebot"]

[tool.ruff]
line-length = 100
Expand Down
3 changes: 0 additions & 3 deletions src/gaggibot/__init__.py

This file was deleted.

3 changes: 3 additions & 0 deletions src/matebot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""matebot — the proactive companion for GaggiMate espresso machines."""

__version__ = "0.1.0"
File renamed without changes.
10 changes: 5 additions & 5 deletions src/gaggibot/cli.py → src/matebot/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""gaggibot CLI: run / decode / sitegen / sync."""
"""matebot CLI: run / decode / sitegen / sync."""

from __future__ import annotations

Expand All @@ -15,10 +15,10 @@

def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="gaggibot",
prog="matebot",
description="The proactive companion for GaggiMate espresso machines.",
)
parser.add_argument("--version", action="version", version=f"gaggibot {__version__}")
parser.add_argument("--version", action="version", version=f"matebot {__version__}")
parser.add_argument("--config", help="path to config.toml")
parser.add_argument("-v", "--verbose", action="store_true")
sub = parser.add_subparsers(dest="cmd", required=True)
Expand Down Expand Up @@ -72,7 +72,7 @@ async def _sync(config: Config) -> int:
from .sync import sync

if not config.data_repo:
print("error: GAGGIBOT_DATA_REPO / data_repo not configured", file=sys.stderr)
print("error: MATEBOT_DATA_REPO / data_repo not configured", file=sys.stderr)
return 1
async with GaggiMateClient(config.machine_host) as client:
# WS connection (for profiles) is optional here; HTTP does the rest.
Expand All @@ -90,7 +90,7 @@ async def _run(config: Config, *, replay: str | None, dry_run: bool) -> int:
from .sync import sync_soon
from .watcher import ShotWatcher, replay_frames

log = logging.getLogger("gaggibot")
log = logging.getLogger("matebot")
state = State(pathlib.Path(config.state_dir) / "state.json")

async with GaggiMateClient(config.machine_host) as client:
Expand Down
2 changes: 1 addition & 1 deletion src/gaggibot/commands.py → src/matebot/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async def handle(self, text: str) -> bool:
handler = getattr(self, f"_cmd_{cmd}", None)
if handler is None:
if cmd == "start":
await self.messenger.send("☕ gaggibot at your service.\n\n" + HELP)
await self.messenger.send("☕ matebot at your service.\n\n" + HELP)
return True
return False
try:
Expand Down
Loading
Loading