从零搭建BrewUI:开源家酿啤酒酿造管理工具实战指南 上周把一批浑浊IPA装瓶的时候朋友问我要不要换一套商用酿造管理软件我指了指屏幕上的BrewUI说算了我自己捣鼓的那个界面虽然丑但它懂我这套小破设备。这话不是傲娇是真的用顺手了。今天想认认真真把这个叫BrewUI的开源酿造管理工具聊透——它不是那种需要你重新买一堆配套硬件的商业平台而是一个能自己跑在家里服务器上的Web UI专门用来管配方、跟踪发酵温度、记录装瓶日期、算酒精度预估顺便还能把发酵罐的探头数据画成曲线。你要是玩家酿或者想给家里那套温控设备加个“脸”那这个项目大概率能省掉你一冰箱的贴纸和Excel表格。我最早看上BrewUI是因为一个很现实的痛点配方散在手机便签里发酵记录写在冰箱贴背面温度数据存在温控插座App的云端每次想复盘上一批酒的操作得翻三个地方还不一定找得到。BrewUI把这件事收拢成了一种很舒服的工作流——配方、批次、设备数据、操作日志全在一个浏览器页面里解决而且它不对你的酿造方式指手画脚你就是煮出糖化桶的、还是BIAB袋中酿造一次性搞定的它都只是安静当你的记录本和仪表盘。这篇文章我会把它按我自己实际搭起来的过程拆开讲先说我为什么这么设计整体结构再讲配方数据模型怎么建才不出乱子然后给出完整的搭建步骤和核心代码最后把我踩过的坑和排查方法一条条列出来。不管你是第一次听说这个工具还是已经在用但想优化使用方式应该都能从中找到点能直接抄作业的东西。1. 项目思路与整体设计拆解1.1 一个“酿造日志”到底该管些什么BrewUI表面上是个UI本质上是个围绕“批次”展开的酿造数据库。所谓批次就是你某一天煮的那一锅麦汁从配方设计到装瓶喝掉的全过程记录。理解了这个概念后面所有功能都好推断它得有配方管理因为一批酒从哪里来要说得清它得有发酵跟踪因为麦汁进去到酒出来之间那十天半个月温度变化比什么都重要它得有操作日志因为哪天干了干投、哪天调的温控、哪天转桶这些时间点对判断风味走向至关重要它还得有库存概念麦芽、酒花、酵母用掉了多少心里得有数。所以BrewUI解决的并不是“酿造自动化”那么大的命题。它更像一个驾驶舱仪表盘只负责把过程中产生的数据可视化、结构化、可追溯。这和很多嵌入式酿造控制器有本质区别——控制逻辑仍然可以留在你的温控插座或继电器模组里BrewUI只和你“沟通”这些数据。这种设计很聪明因为你不需要因为它放弃已有的温控设备。我选择它而不是直接用现成的商业配方管理软件原因是那类软件往往把重心放在“云端菜谱库”和“社交分享”上反而弱化了本地设备对接能力。BrewUI因为在本地跑API开放想怎么接都行而且数据都在自己家里不用怕第三方平台哪天关停。1.2 技术栈选型为什么是Web UI而不是桌面程序BrewUI这类项目选择Web技术栈不是偶然。家庭酿造的监控场景里你会想用手机看发酵温度也会想在书房电脑上编辑配方偶尔还可能在客厅平板上瞄一眼曲线。Web UI天然跨平台一个浏览器到处能开不用为每个操作系统单独打包。后端用Python写具体框架我这边用的是Flask再加SQLite做数据存储。有人会觉得这组合不“高级”但家酿数据量真的不大——高性能数据库完全是浪费。SQLite单文件备份也方便整个数据库就是一个文件拷走就是备份。前端则是经典的HTML加JavaScript配一个图表库画趋势曲线。如果你不想手写前端逻辑可以直接套用BrewUI自带的页面模板再用Fetch调用后端接口即可。这套选型最大的好处是依赖少部署简单一个树莓派或者家里那台常年开机的NAS都能跑起来。你不需要装MySQL不需要装Redis更不需要Kubernetes——所有组件一只手数得过来。对于周末才有时间酿酒的爱好者来说维护成本越低工具才越可能被持续用下去。注意如果你完全不会Python后端也没关系。BrewUI的核心是那些数据表和API约定照着我下面的思路复制结构一样能跑通。1.3 核心功能模块拆解我从使用频率从高到低把BrewUI的实际功能分成四个模块批次总览列出所有酿造批次状态一目了然计划中、糖化中、发酵中、熟成中、已装瓶、已喝光。这是每天打开页面第一眼看到的东西。配方编辑器管理麦芽、酒花、酵母、水的用量和参数自动算出初始糖度预估、苦度IBU和酒精度ABV预估值。发酵监控看板展示当前发酵批次的历史温度曲线、目标温度区间如果接了温度探头还能实时读到数据。操作日志与库存按时间线记录每次操作顺便扣减原料库存避免出现“以为还有酒花结果翻柜子只剩半包”的尴尬。这四个模块如果拆开做每一个都是很传统的CRUD应用但它们组合在一起并且围绕“批次”这条主线串起来之后体验就和零散的Excel完全不一样了。任何一次操作无论在哪个环节都会被归到具体的批次下复盘时按时间线一拉整个过程清清楚楚。2. 数据模型与核心细节解析2.1 配方表设计别把麦芽和酒花挤在同一行这是BrewUI设计里我觉得最值得抄的地方配方不是“一张表里一行一个配方”而是拆成了配方主表和配方原料明细表。为什么这么做因为一个配方里有麦芽、酒花、辅料、酵母每种原料的数量和单位不一样操作时间和添加顺序也不一样。如果把所有信息塞进一行JSON字段里后续想做统计比如“我今年用了多少卡斯凯特酒花”就会非常痛苦。主表只存配方的基本信息名称、风格、目标体积、煮沸时间、效率、备注。明细表则用“原料类型”字段区分麦芽、酒花、酵母、其他添加物每行记录原料名称、用量、添加时间点、用途。这样查询起来简单扩展也容易。以后想加个“水质调整”模块直接在明细表里加类型就行不需要改表结构。数据表结构大致可以这样理解CREATE TABLE recipes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, style TEXT, batch_volume_l REAL DEFAULT 20.0, boil_time_min INTEGER DEFAULT 60, brewhouse_efficiency REAL DEFAULT 0.75, created_at TEXT DEFAULT (datetime(now)) ); CREATE TABLE recipe_ingredients ( id INTEGER PRIMARY KEY AUTOINCREMENT, recipe_id INTEGER NOT NULL, ingredient_type TEXT NOT NULL, -- malt, hop, yeast, adjunct, water name TEXT NOT NULL, amount_kg REAL, -- for malt/adjunct amount_g REAL, -- for hops time_min INTEGER, -- boil time add point for hops use TEXT, -- e.g. mash, boil, dry hop, flameout note TEXT, FOREIGN KEY (recipe_id) REFERENCES recipes(id) );最初一版我把麦芽和酒花放在同一张原料表里统一管理只靠类型字段区分。后来发现计算IBU时需要遍历所有酒花行并带上时间参数计算糖度预估时需要把所有麦芽行的大比重相加如果硬塞在一个表里做各种条件过滤性能倒不是问题但代码逻辑会绕。拆开之后计算函数写起来清楚得多扩展也容易。2.2 糖度、苦度和酒精度这三个数字怎么算BrewUI的配方编辑器里最常被问到的就是那个ABV预估值准不准我的回答是它只是估算但足够给个心理预期。这里我分享一下我实际写进代码里的三个算法不算多严谨但家酿场景下完全够用。糖度预估麦芽的潜在比重取决于它的“每千克每升能贡献多少糖度”即“每磅每加仑”的换算。欧洲家酿圈常用“克/升”的方式。简化版公式是把配方里所有麦芽重量乘以其潜在比重系数再除以批次体积乘以效率最后换算成比重。这里我会在计算时先假设100%效率再乘一个糖化效率系数。这样当你自算的和实际测量差别很大时就知道是效率参数该调了而不是公式错了。苦度估算这里我采用的是Tinseth公式是目前家酿圈用得比较广的一种。核心思路是酒花的苦味酸异构化取决于煮沸时间和麦汁比重时间越长、比重越低苦味利用率越高。实际代码里会遍历所有酒花添加记录把每次添加的克数乘上其阿尔法酸百分比再乘上利用率最后除以批次体积。这个算法出来的IBU虽然不是实验室级的精确但你在设计配方时用来比较“这版比上版苦多少”是完全可用的。酒精度预估最常见的公式是ABV (OG - FG) * 131.25。OG是初始比重FG是终点比重如果你还没发酵完FG就只能靠经验猜——通常是OG的1/4到1/6往下掉。BrewUI的做法是让用户手动输入预估FG或者选择“按酵母平均衰减率自动估算”。我更推荐前者因为酵母状态对衰减的影响太大了靠猜不如靠记录。提示把计算公式写出来之后建议一定在界面上标注“此值为理论预估”防止新手把IBU预估值当成最终啤酒的真实苦味程度。2.3 温度数据的采集与曲线展示发酵监控这块我踩过的坑最多。BrewUI本身不负责采集温度它只负责“显示”温度。数据从哪来通常有两条路一是你手动拿温度计读了填进去二是通过API把温度探头的数据POST进来。前者适合没有智能设备的新手后者适合已经用ESP32/树莓派接了DS18B20温度探头的老玩家。我在项目里选了DS18B20配ESP32的路线因为这东西便宜又稳定一条总线上能挂多个探头测发酵液温度和环境温度都够用。ESP32每5分钟读一次探头数据通过HTTP POST到BrewUI的/api/batches/id/temperature接口。接口收到数据后把它存进表里前端图表实时刷新。这套链路看起来简单但有几个容易出问题的地方我在后面“问题排查”部分会详细说。先看这个API接收端的参考实现app.route(/api/batches/int:batch_id/temperature, methods[POST]) def add_temperature(batch_id): data request.get_json(forceTrue) temp_c data.get(temperature_c) if temp_c is None: return jsonify({error: missing temperature_c}), 400 timestamp data.get(timestamp, datetime.utcnow().isoformat()) cur db.execute( INSERT INTO fermentation_log (batch_id, temperature_c, timestamp) VALUES (?, ?, ?), (batch_id, temp_c, timestamp) ) db.commit() return jsonify({status: ok}), 201前端刷新的逻辑我建议用定时器每30秒拉一次最近24小时的数据不要用WebSocket实时推送。为什么家庭环境网络偶尔不稳定定时轮询断了还能自愈WebSocket一旦断开重连逻辑写不好图表就卡死在那了。后期如果想做推送通知可以再在服务端加一层定时检查最新数据的逻辑不必为了“实时”而简化可靠性。3. 实操过程与核心环节实现3.1 本地环境从零搭建如果你想把BrewUI跑起来第一步是准备环境。我这边用的是Python 3.10版本原因无他兼容性最稳。先建虚拟环境再把依赖装好。核心依赖就三个Flask、Flask-CORS如果你要用ESP32跨域POST、以及一个SQLite驱动库Python自带的sqlite3其实就够了。依赖安装命令python3 -m venv brewui-env source brewui-env/bin/activate pip install flask flask-cors装完之后项目目录我会这样规划app.py放所有后端路由和API逻辑schema.sql放建表语句static/放前端页面和JavaScriptdata/放SQLite数据库文件。这种分法清晰后续不管是备份还是迁移都很方便。运行服务前先把数据库建好sqlite3 data/brewui.db schema.sql然后启动Flask服务export FLASK_APPapp.py export FLASK_ENVdevelopment flask run --host0.0.0.0 --port5000--host0.0.0.0这个参数很重要不然你只能在本机访问手机和平板根本连不上。如果跑在树莓派上局域网内其他设备就可以通过http://树莓派IP:5000访问BrewUI界面了。3.2 后端API设计围绕批次组织一切BrewUI的API我不打算设计成那种RESTful纯资源风格而是更偏“业务动作”一点。比如创建批次、添加配方、记录温度、更新批次状态每个动作对应一个清晰的路由。这样的好处是前端调用时好理解ESP32之类的设备对接时也不需要去猜资源层级。我实际用的几个核心路由POST /api/batches创建批次并绑定配方GET /api/batches获取批次列表GET /api/batches/id获取批次详情含配方、日志、温度PATCH /api/batches/id更新批次状态POST /api/batches/id/temperature上报温度POST /api/batches/id/log添加操作日志创建批次这个接口我选择让它在创建时就把配方ID带进来避免先建空批次再绑定配方这种两步操作。如果你已经有一段自己积累的配方数据创建批次时从下拉框里选配方会比从空白开始舒服得多。打个比方创建一个新批次的请求体大致是{ recipe_id: 1, batch_name: 2025-06-15 西楚单花IPA, start_date: 2025-06-15, notes: 这批用了一包新到的西楚酒花 }后端收到后建批次记录并把状态初始化为“计划中”。接下来你可以按实际过程逐步把状态改成“糖化中”“发酵中”“已装瓶”。每一步更新都会在操作日志里自动加一条记录这个“自动生成日志”的小细节后期复盘时真的省了很多回忆的时间。3.3 前端页面快速组装BrewUI的前端我并没有用很重的框架就是原生JavaScript加一个图表库我这里用的是Chart.js。为什么不选Vue或者React对一个设备管理和记录型工具来说页面状态并不复杂用框架反而得维护构建流程、打包配置对一个周末项目来说负担大于收益。原生JS加少量模块化代码改起来直接刷新浏览器就能看到效果非常轻快。主界面我拆成三块左侧是批次列表中间是当前选中的批次详情右侧是温度曲线图表。这种经典“列表-详情”布局的好处是信息密度高手机上竖屏看也不会太拥挤。温度曲线这块Chart.js的折线图就能满足。从后端拉来最近24小时或7天的温度数据横轴是时间纵轴是温度再把目标温度区间的上下限画成两条水平参考线。一个很实用的优化是在图表里用半透明色块标出目标区间看一眼就知道当前温度是偏高、偏低还是在正常范围内。这个用Chart.js的chartjs-plugin-annotation插件就能实现几行配置的事。3.4 把真实温控设备接进来如果你已经有温控插座或继电器温控器BrewUI不需要你去改动它的控制逻辑只需要把它的数据“读”出来。这里我以最常见的ESP32加DS18B20方案为例分享一个最简的数据上报固件逻辑。大致思路是ESP32连接WiFi每5分钟读一次DS18B20温度通过HTTP POST到BrewUI的API。如果POST失败就在本地缓冲区存一下等网络恢复后补传。这个“补传”逻辑非常重要——不然WiFi闪断的那十几分钟数据就丢了曲线会断一截。一个非常简化的ESP32端代码逻辑MicroPythonimport urequests, time, onewire, ds18x20 while True: temp read_temperature() # 读取DS18B20 payload {temperature_c: temp} try: resp urequests.post( http://192.168.1.100:5000/api/batches/1/temperature, jsonpayload, timeout5 ) resp.close() except Exception as e: print(上报失败稍后重试, e) time.sleep(300)注意这个代码里我没写重传逻辑实际项目里建议把读到的温度先存到一个循环缓冲区等上报成功后才清掉这样网络抖动也不会丢数据曲线更完整。温度探头的防水探头记得买不锈钢封装的直接泡在发酵液里测比贴在罐壁上测准确得多。4. 常见问题与排查技巧实录4.1 温度曲线出现断崖或者空洞这是我用了两周后最先遇到的问题。曲线“断崖”通常是设备端上报失败比如ESP32掉线、网络抖动、或者服务端在重启。排查思路是先看设备端日志确认有没有报错再看服务端数据库里是不是确实缺了那段时间的数据。如果是设备端上报失败优先检查WiFi信号尤其是发酵罐放在地下室的时候信号穿墙损耗特别大。我的建议是给ESP32加一根外置天线或者把它挪到离路由器近一点的位置探头线延长一点没关系信号稳定更重要。另一种“曲线空洞”是数据库里没数据但设备端日志显示上报成功这种情况大概率是时间戳问题。我第一次写上报逻辑时在ESP32端用了本地时钟但没做NTP对时结果设备跑了几天之后时钟慢慢漂移了几个小时导致数据被记到了错误的时间点图表上对不上。后来我改成了在POST请求里不传时间戳让服务端统一用接收请求的服务器时间为准问题就再没出现过。注意如果你上传的数据时间戳来自不同设备一定要统一用UTC或统一用北京时间千万别混着来否则你的曲线会像被猫抓过一样乱。4.2 配方里的ABV预估值和实际测量值差很多这个问题九成出在“终点糖度靠猜”这个环节。BrewUI的ABV公式本身没问题问题是当你还没发酵完的时候FG只能靠经验填。我见过最多的场景是用户配方算出来预计ABV有6.8%实际一发结束测出来只有5.6%他以为是软件算错了其实是他把预估FG填得太低或者他酵母根本没发酵到预期衰减率。排查思路先看实际OG和FG是多少算一下真实衰减率再反推是不是酵母活力不够比如过期酵母、发酵温度偏低。如果这批酒没问题那就把配方的“酵母衰减率”参数记下来下次做类似批次时填一个更贴近历史的数值预估值就会准很多。BrewUI这类的工具本质上数据越攒越准前期预估偏差大是正常的。4.3 设备上报数据但页面不更新网页不更新但数据库里有数据通常是前端缓存问题或定时刷新逻辑写错了。我自己遇到过一种很隐蔽的情况Chart.js实例创建后如果后续更新数据时没有先destroy()旧实例图表就会一直停留在第一次渲染的状态。后来我在每次重新拉数据前先把图表实例销毁重建问题解决。还有一个常见原因是浏览器缓存了旧的JS文件。开发时开着浏览器开发者工具、勾选“禁用缓存”可以避免大部分这类困扰。部署给手机端用的时候如果手机浏览器一直显示旧界面试着强制刷新或者清一下缓存立竿见影。4.4 操作日志时间线和实际酿造过程对不上这个问题大多是“补记”造成的。人总有忙的时候发酵第3天忘了记干投第5天想起来了才补上。但补记录时会遇到一个选择日志时间按“实际干投那天”填还是按“录入系统的当前时间”填如果你选了后者后来复盘时看到的时间线就和真实过程对不上。我的经验是操作日志的时间应该尽量填写实际发生的时间而不是录入时间系统可以把“录入时间”单独存成一个字段作为审计用但显示时以实际发生时间为准。另外一个小建议批次状态更新的同时尽量写一句话的备注。比如“从发酵中改为熟成中”这个状态变化本身没有情绪但如果你在同一时间点加一句“尝了一口酒花香气很足但是有一点双乙酰的味道”那三个月后再看这批酒你会瞬间想起当时的判断依据这对你改进配方有很大的帮助。4.5 不小心删了数据库怎么办这其实不是BrewUI特有的问题但因为我被坑过所以特别提醒一下。SQLite数据库就是一个单文件放在data/brewui.db里。很多新手会手动去编辑这个文件或者直接把别人的db文件拷贝过来覆盖结果格式对不上启动就报错。我的建议是数据库文件是程序在管不是让人直接上手改的。要备份直接复制这个文件就行要迁移直接把文件拷到新机器上。所有修改都应该通过页面操作或API完成避免底层的字段错乱。如果你实在想修改历史数据可以先停掉服务备份原文件再用SQLite的命令行工具谨慎操作改完立即测试页面是否正常。这套流程我屡试不爽。另外如果日志太多导致数据库文件涨到几百兆也不用慌。定期清理掉超过两年的历史数据或者把不常看的批次归档到一个单独的表中能让页面加载速度保持轻快。家酿数据量再大也是有限的一般用不上什么复杂的优化技巧。5. 一点进阶玩法与最后的经验跑顺基础功能之后BrewUI能玩的花样还有不少。我个人最推荐接一个钉钉或者企业微信的机器人Webhook让BrewUI在温度超出目标区间时主动推送一条消息到手机上。做这件事不需要改BrewUI太多东西只需要写一个后台轮询脚本每5分钟查一次最新温度是否超出区间超了就调Webhook接口发送通知。这比一直盯着曲线省心太多了尤其是当你白天上班家里的发酵罐在30度室温下可能需要你远程开空调降温的时候。还有一个非常实用的扩展让BrewUI对接你的电子秤。很多家酿玩家有带蓝牙的电子秤可以把糖化桶的总重量数据上传到BrewUI把麦汁体积变化也画成曲线。不过这个改动量稍微大一点需要你愿意去折腾硬件端的蓝牙网关和数据结构设计。我目前的版本还没做这块但已经在计划列表里了。最后再分享一个小技巧。BrewUI这种工具真正好用的状态不是“功能多”而是“打开次数多”。很多功能强大的酿造软件最后被弃用都是因为界面信息密度太低——点好几下才能看到自己想知道的内容久而久之就懒得打开了。所以我自己在迭代BrewUI时坚持一个原则首页必须在5秒内让人看清当前有几批酒在发酵、温度是否正常、下一步该干什么。你可能觉得这不算什么技术含量但真用起来这种克制才是工具能活下来的关键。如果你也打算自建一套BrewUI我强烈建议你从最简单的版本开始不要一开始就追求接设备、画曲线、做推送。先手动记录两个批次的配方和发酵温度把数据模型跑顺了再逐步接入自动化。这样你每一步都清楚自己在做什么也不会在排错时被一堆噪音干扰。家酿的乐趣一半在酒里一半在过程里工具只是帮你把过程记得更清楚罢了。