From cc11dffb02345cb5133551995153da719bac320e Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Mon, 29 Jun 2026 11:52:41 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20CLAUDE.md=20?= =?UTF-8?q?=E4=B8=8E=20README=20=E5=AF=B9=E9=BD=90=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补充天气/趋势两套文件缓存机制说明(为 cron 每分钟触发而设) - 天气接入真实和风 API + 矢量手绘图标,修正原 mock 占位描述 - 修正 jm_api 为免登录 GET(带 clientid 头)、布局四区、回退占位 - 更新 cron 部署为 run.sh、补全环境变量与 gitignore 清单 Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 16 +++++++++---- README.md | 68 +++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index eeeb8e4..92190cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,20 +15,26 @@ pip install -r requirements.txt # 仅 Pillow>=10.0;urllib 用标准库 python3 main.py # 取数据 → 渲染 → 推送(设备取列表第一个) python3 main.py --render-only # 只渲染到 output/dashboard.png,不推送(本地调试渲染时用这个) +sh run.sh # cron 包装脚本:cd 项目目录 + vfox python 绝对路径 + 追加日志到 output/cron.log + # 环境变量覆盖配置(见 config.py) ZECTRIX_API_KEY=zt_xxx ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 main.py ``` 无测试、无 lint。调试渲染效果就跑 `--render-only` 看 `output/dashboard.png`。 +**定时部署**:`run.sh` 是 cron 入口(已在 `.gitignore`,本机专用)。cron 不继承登录环境,故脚本内 python 写死 vfox 绝对路径、并 `cd` 到项目目录后追加日志到 `output/cron.log`。设计为**每分钟**触发:业务数据每次都刷新,天气/趋势靠缓存避免高频打接口(见 `data.py`)。可覆盖的环境变量不止 ZECTRIX 两项,还有 `JM_BASE_API`/`JM_CLIENT_ID`/`QWEATHER_HOST`/`QWEATHER_KEY`/`WEATHER_LOCATION`/`WEATHER_TTL`(详见 `config.py`)。 + ## 架构(数据 → 渲染 → 推送 三层) `main.py` 串联三层,各层职责单一、低耦合: -- **`data.py`** — 唯一数据入口 `get_dashboard_data()`,返回固定结构的 dict。日期/时间用真实系统时间;橘喵今日经营+趋势由 `_mao()` 经 `jm_api`、天气由 `_weather()` 经 `weather_api`(和风,定位南京·秦淮)拉真实数据,**任何接口异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可(renderer 依赖其 key,尤其 `mao.trend.series` 每条须带 `style`;环比/同比 `(direction, text)`,direction 为 `None` 时不画涨跌三角)。 -- **`jm_api.py`** — jm-devops 后端统计接口客户端,**免鉴权 GET**(`/system/statistics/realtime`、`/recent-days?days=`)。注:早期曾实现登录+RSA/AES 加密+验证码 OCR,后改为后端直接放开这两个接口,故已全部删除——若后端再次收紧鉴权,参考 jm-devops 的 `src/utils/{crypto,jsencrypt,request}.ts` 复刻。 -- **`weather_api.py`** — 和风天气(QWeather)客户端。用**用户专属 API Host**(`QWEATHER_HOST`)+ `X-QW-Api-Key` 头鉴权;响应 **gzip 压缩**(按 magic number 手动解压)、返回 `code` 为**字符串**。`get_now()` 取实时(`now.text` 中文天气 / `now.temp`),`get_today()` 取 `/3d` 的 `daily[0]` 今日温区。`QWEATHER_HOST`/`QWEATHER_KEY` 未配置时直接抛错→天气区显示 `"--"`。 -- **`renderer.py`** — `DashboardRenderer.render(data)` 返回 PIL `'L'` 模式纯 0/255 图;`render_to_file()` 落盘。布局硬编码为三区:大时钟+日期 / 天气 / 橘喵三列 / 近30天趋势折线图。趋势线用 `_styled_polyline()` 按 solid/dashed/dotted 区分(设备无颜色),各指标按自身极值独立归一化(故只看趋势形状、不可横向比绝对值)。 +- **`data.py`** — 唯一数据入口 `get_dashboard_data()`,返回固定结构的 dict。日期/时间用真实系统时间;橘喵今日经营+趋势由 `_mao()` 经 `jm_api`、天气由 `_weather()` 经 `weather_api`(和风,定位南京·秦淮)拉真实数据,**任何接口异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可(renderer 依赖其 key,尤其 `mao.trend.series` 每条须带 `style`、`weather.icon` 为和风图标代码;环比/同比 `(direction, text)`,direction 为 `None` 时不画涨跌三角)。 + - **两套文件缓存**(为 cron 每分钟独立进程而设,必须落盘):天气缓存 `.weather_cache.json`(TTL=`WEATHER_TTL`,默认 15min;未过期不发请求);趋势缓存 `.trend_cache.json`(按日期 key,当天命中即不再调 `recent-days`,每天只拉一次)。两者**拉取失败均沿用旧缓存**避免整片 `"--"`,仍无缓存才回退占位。两个缓存文件已在 `.gitignore`。 +- **`jm_api.py`** — jm-devops 后端统计接口客户端,**免登录 GET**(`/system/statistics/realtime`、`/recent-days?days=`),仅带 `clientid` 头(`config.CLIENT_ID`)、无 token。注:早期曾实现登录+RSA/AES 加密+验证码 OCR,后改为后端直接放开这两个接口,故已全部删除——若后端再次收紧鉴权,参考 jm-devops 的 `src/utils/{crypto,jsencrypt,request}.ts` 复刻。 +- **`weather_api.py`** — 和风天气(QWeather)客户端。用**用户专属 API Host**(`QWEATHER_HOST`)+ `X-QW-Api-Key` 头鉴权;响应 **gzip 压缩**(按 magic number 手动解压)、返回 `code` 为**字符串**。`get_now()` 取实时(`now.text` 中文天气 / `now.temp` / `now.icon` 图标代码),`get_today()` 取 `/3d` 的 `daily[0]` 今日温区。`QWEATHER_HOST`/`QWEATHER_KEY` 未配置时直接抛错→天气区显示 `"--"`。 +- **`renderer.py`** — `DashboardRenderer.render(data)` 返回 PIL `'L'` 模式纯 0/255 图;`render_to_file()` 落盘。布局硬编码为四区(render 内 A/B/C/D):A 大时钟+日期 / B 天气 / C 橘喵今日三列 / D 近30天趋势折线图。趋势线用 `_styled_polyline()` 按 solid/dashed/dotted 区分(设备无颜色),各指标按自身极值独立归一化(故只看趋势形状、不可横向比绝对值)。 + - **天气图标全矢量手绘**(无图片资源):`_weather_category()` 把和风 icon 代码归并为 8 类(sunny/clear_night/cloudy/overcast/thunder/rain/snow/fog,识别不了回退 cloudy),再由 `_weather_icon()` 用 `_sun`/`_cloud` 等基元按 1bit 描边画出;改天气展示从这里动。 - **`pusher.py`** — `push_image()` 走 Zectrix API(`POST /devices/{id}/display/image`,手写 multipart)。`resolve_device_id()` 在 `config.DEVICE_ID` 为空时自动取设备列表第一个。 - **`config.py`** — API、设备、画布尺寸、字体路径、推送参数;敏感项可被环境变量覆盖。 @@ -44,4 +50,4 @@ ZECTRIX_API_KEY=zt_xxx ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 main.py ## 注意 - `config.py` 内置了一个默认 `API_KEY`(硬编码 fallback)。修改/分享代码时留意,优先用 `ZECTRIX_API_KEY` 环境变量。 -- `output/` 与 `__pycache__/` 已在 `.gitignore` 中忽略。 +- `.gitignore` 已忽略:`output/`、`__pycache__/`、`*.pyc`、两个缓存文件 `.weather_cache.json`/`.trend_cache.json`、本机专用的 `run.sh`。改缓存逻辑后想强制重拉,删掉对应缓存文件即可。 diff --git a/README.md b/README.md index 6da3256..1f1ba31 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,24 @@ ## 效果 -- 顶部:大号时钟 + 日期 | 天气(图标 + 温度 + 区间) -- 中部:橘喵今日经营(单量 / 流水 / 毛利,含环比、同比涨跌) +- 顶部:大号时钟 + 日期 | 天气(矢量手绘图标 + 温度 + 天气/温区) +- 中部:橘喵今日实时经营(单量 / 流水 / 毛利,含环比、同比涨跌) - 底部:近30天三指标趋势折线图(同一图表,实线 / 虚线 / 点线区分,X 轴仅月/日) ## 目录结构 ``` eink-push/ -├── main.py # 入口:取数据 → 渲染 → 推送 -├── config.py # 配置:API Key、设备、字体、尺寸、推送参数 -├── data.py # 数据层:拉真实数据并映射,失败回退 mock -├── jm_api.py # jm-devops 后端统计接口客户端(免鉴权 GET) -├── renderer.py # 渲染层:DashboardRenderer 生成 400×300 图片 -├── pusher.py # 推送层:设备列表 / 推送图片(标准库 urllib) -├── fonts/ # Fusion Pixel 点阵字体(OFL 协议) -├── output/ # 生成的图片(git 忽略) +├── main.py # 入口:取数据 → 渲染 → 推送 +├── config.py # 配置:API Key、设备、字体、尺寸、推送参数、缓存路径 +├── data.py # 数据层:拉真实数据并映射,失败回退占位 "--"(不回退 mock);含天气/趋势缓存 +├── jm_api.py # jm-devops 后端统计接口客户端(免登录 GET,仅带 clientid 头) +├── weather_api.py # 和风天气(QWeather)客户端(实时天气 + 今日温区) +├── renderer.py # 渲染层:DashboardRenderer 生成 400×300 纯黑白图片 +├── pusher.py # 推送层:设备列表 / 推送图片(标准库 urllib) +├── run.sh # cron 包装脚本(本机专用,git 忽略) +├── fonts/ # Fusion Pixel 点阵字体(OFL 协议) +├── output/ # 生成的图片与 cron.log(git 忽略) ├── requirements.txt └── README.md ``` @@ -30,43 +32,65 @@ eink-push/ ## 安装 ```bash -pip install -r requirements.txt +pip install -r requirements.txt # 仅 Pillow>=10.0;网络请求用标准库 urllib ``` ## 使用 ```bash python3 main.py # 渲染并推送(默认取设备列表第一个) -python3 main.py --render-only # 只渲染到 output/dashboard.png,不推送 +python3 main.py --render-only # 只渲染到 output/dashboard.png,不推送(本地调试渲染用) ``` -环境变量可覆盖配置: +环境变量可覆盖配置(详见 `config.py`): ```bash +# Zectrix 设备 ZECTRIX_API_KEY=zt_xxx ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 main.py +# jm-devops 后端 / 和风天气 +JM_BASE_API=... JM_CLIENT_ID=... +QWEATHER_HOST=... QWEATHER_KEY=... WEATHER_LOCATION=经度,纬度 WEATHER_TTL=900 ``` -## 定时刷新(cron 示例,每 15 分钟) +## 定时刷新(cron) + +设计为**每分钟**触发:业务数据每次都刷新,天气/趋势靠缓存避免高频打接口(见下「设计说明」)。 +`run.sh` 是 cron 入口——cron 不继承登录环境,故脚本内 python 写死绝对路径、`cd` 到项目目录后追加日志到 `output/cron.log`。 ```cron -*/15 * * * * cd /path/to/eink-push && /usr/bin/python3 main.py >> /tmp/eink.log 2>&1 +* * * * * /path/to/eink-push/run.sh ``` +手动测试:`sh run.sh`。 + ## 数据来源 -- **橘喵今日经营 + 近30天趋势**:已接入 jm-devops 后端统计接口(`jm_api.py`), - 免鉴权直接 GET: +- **橘喵今日经营 + 近30天趋势**:接入 jm-devops 后端统计接口(`jm_api.py`), + 免登录 GET(仅带 `clientid` 头、无 token): - `GET /system/statistics/realtime` —— 今日单量/流水/毛利 + 环比/同比 - - `GET /system/statistics/recent-days?days=30` —— 近30天每日数据 - `data.py` 负责把响应映射成渲染结构;**拉取失败会自动回退 mock**, - 设 `JM_USE_MOCK=1` 可强制用 mock 离线调试。后端地址等见 `config.py`。 -- **天气**:`data.py` 的 `_weather()` 仍为 mock 占位,接入真实天气 API 时替换它即可。 + - `GET /system/statistics/recent-days?days=30` —— 近30天每日数据(按日期降序、不含今天) +- **天气**:接入真实和风天气(QWeather,`weather_api.py`),定位固定南京·秦淮。 + 实时天气取 `now.text`/`now.temp`/`now.icon`,今日温区取 `/3d` 的 `daily[0]`。 + 需配置 `QWEATHER_HOST` + `QWEATHER_KEY`(控制台-设置查看)。 - **日期 / 时间 / 上次更新**:真实系统时间。 +> `data.py` 负责把响应映射成渲染结构;**任何接口失败都回退占位 `"--"`,不回退 mock**。 + +## 缓存机制 + +为「每分钟触发、但天气/趋势无需高频刷新」而设的两套**文件缓存**(cron 每次独立进程,必须落盘): + +- **天气缓存** `.weather_cache.json`:TTL = `WEATHER_TTL`(默认 15 分钟),未过期不发请求。 +- **趋势缓存** `.trend_cache.json`:按日期为 key,当天命中即不再调 `recent-days`,每天只拉一次。 + +两者拉取失败均**沿用旧缓存**避免趋势/天气区整片 `"--"`,仍无缓存才回退占位。想强制重拉删掉对应缓存文件即可(均已 git 忽略)。 + ## 设计说明 - 设备为 **1bit 无真灰阶**,灰阶只能抖动成网点(难看),故全程纯黑白,靠构图、字号、留白建立层级。 - 字体用 **Fusion Pixel 12px 点阵字体**,按整数倍尺寸(12/24/48)渲染、关闭抗锯齿 → 像素锐利; 重点数据用「伪粗体」(偏移叠绘)加粗。 +- 天气图标为**全矢量手绘**(无图片资源):按和风 icon 代码归并为 8 类(晴/晴夜/多云/阴/雷/雨/雪/雾),用基本图元按 1bit 描边画出。 +- 趋势折线各指标按自身极值独立归一化,故只表达**趋势形状**,不可横向比绝对值。 - 推送用 `dither=false`(硬阈值),纯黑白图最锐利。 -``` +``` \ No newline at end of file