feat: 版式重构为三段 + 接入 Claude Usage(只读本地) + 定位江宁
将仪表盘从「四区(时钟/天气/今日实时/30天趋势)」重构为上中下三段: - A 顶部:大时钟+日期 / 天气(定位改南京·江宁) - B 中部主角 Claude Usage:5h/7d 用量进度条 + 大号百分比 + 时间维度对比 pace (pace=用量%−时间%,超前↑红/节余↓黑,参考 claude-statusline);用量≥阈值该条转红预警 - C 底部 橘喵今日实时:三列版式对齐主分支(名F12/值F24粗/环比同比偏移 +18/+34/+64/+82) - 近30天趋势图隐藏(_trend_chart 及依赖保留、未调用,可挂回) 用量只读本地、不发网络请求:新增 usage_local.py 读取 claude-statusline 落盘的 /tmp/claude/statusline-usage-cache.json(utilization + resets_at)。utilization 可能滞后, 但 resets_at 为绝对时间,故 pace 每次按当前时间现算、始终准确;文件缺失回退 "--"。 其他: - 天气定位 秦淮→江宁(config 默认 118.840,31.953) - 进度条边框随条色(红条即红框);行内元素按墨迹竖直中心对齐(_mid) - 新增 F13/F16 字号(非整数倍,fontmode=1 保持纯色) - 全图仍严格三色(黑/白/红)、无灰阶不抖动;底部留 ~11px 下边距 - 同步更新 CLAUDE.md / README.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
24
CLAUDE.md
24
CLAUDE.md
@@ -6,9 +6,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
`eink-push` 是 JM monorepo 中一个**独立的 Python 工具**,与其余 Java/Vue 子项目技术栈无关,不受父级 CLAUDE.md 的 jm-cloud/uniapp/admin 开发约束。
|
||||
|
||||
作用:把橘喵今日经营、近30天趋势、日期与天气合成为一张 **400×300 三色(黑/白/红,BWR)** PNG,推送到 Zectrix 极趣云墨水屏设备。纯 Python + Pillow,**无浏览器依赖**,适合 cron 定时运行。
|
||||
作用:把日期天气、Claude Code 用量、橘喵今日经营合成为一张 **400×300 三色(黑/白/红,BWR)** PNG,推送到 Zectrix 极趣云墨水屏设备。纯 Python + Pillow,**无浏览器依赖**,适合 cron 定时运行。
|
||||
|
||||
> **分支 `feature/colorful`**:新设备在黑白之外支持红色。本分支把渲染从纯黑白升级为黑/白/红三色,红作为**稀缺强调色**只承载语义(方向·警示·强调),占墨极小(实测红≈全图 0.6%、占墨迹 <10%)。仍**无真灰阶、不抖动**。
|
||||
> **分支 `feature/colorful`**:新设备在黑白外支持红色,本分支把渲染升级为黑/白/红三色。当前版式为**上中下三段**:A 大时钟+日期 / 天气(定位南京·江宁);B **Claude Usage** 主角(5h/7d 用量进度条 + 时间维度对比 pace,用量吃紧转红预警);C 橘喵今日实时三列。红作**语义强调**(用量预警、涨、定位、天气太阳/闪电),仍**无真灰阶、不抖动**,全图只三种纯色。近30天趋势图当前**隐藏**(`renderer._trend_chart` 及依赖保留、未调用,随时可挂回)。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -26,20 +26,26 @@ ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 main.py
|
||||
|
||||
无测试、无 lint。调试渲染效果就跑 `--render-only` 看 `output/dashboard.png`。
|
||||
|
||||
**配置统一走 `.env`**:所有凭证/参数放同目录 `.env`(已在 `.gitignore`,本机专用;模板见已提交的 `.env.example`)。`config.py` 顶部用零依赖加载器 `_load_dotenv()` 把 `.env` 写入 `os.environ`,再由各 `os.environ.get(...)` 读取;采用 `setdefault`,故**真实环境变量(cron/命令行注入)优先级高于 `.env`**。`config.py` 内不再保留任何明文凭证(fallback 为空串)。可配置项:`ZECTRIX_API_KEY`/`ZECTRIX_DEVICE_ID`、`JM_BASE_API`/`JM_CLIENT_ID`/`JM_CLIENT_SECRET`/`JM_GRANT_TYPE`/`JM_TENANT_ID`、`QWEATHER_HOST`/`QWEATHER_KEY`/`WEATHER_LOCATION`/`WEATHER_TTL`。
|
||||
**配置统一走 `.env`**:所有凭证/参数放同目录 `.env`(已在 `.gitignore`,本机专用;模板见已提交的 `.env.example`)。`config.py` 顶部用零依赖加载器 `_load_dotenv()` 把 `.env` 写入 `os.environ`,再由各 `os.environ.get(...)` 读取;采用 `setdefault`,故**真实环境变量(cron/命令行注入)优先级高于 `.env`**。`config.py` 内不再保留任何明文凭证(fallback 为空串)。可配置项:`ZECTRIX_API_KEY`/`ZECTRIX_DEVICE_ID`、`JM_BASE_API`/`JM_CLIENT_ID`/`JM_CLIENT_SECRET`/`JM_GRANT_TYPE`/`JM_TENANT_ID`、`QWEATHER_HOST`/`QWEATHER_KEY`/`WEATHER_LOCATION`(默认南京·江宁)/`WEATHER_TTL`、`USAGE_LOCAL_PATH`/`USAGE_WARN`。
|
||||
|
||||
**定时部署**:`run.sh` 是 cron 入口(已在 `.gitignore`,本机专用)。cron 不继承登录环境,故脚本内 python 写死 vfox 绝对路径、并 `cd` 到项目目录后追加日志到 `output/cron.log`(`cd` 后 `config.py` 才能就近找到 `.env`)。设计为**每分钟**触发:业务数据每次都刷新,天气/趋势靠缓存避免高频打接口(见 `data.py`)。
|
||||
**定时部署**:`run.sh` 是 cron 入口(已在 `.gitignore`,本机专用)。cron 不继承登录环境,故脚本内 python 写死 vfox 绝对路径、并 `cd` 到项目目录后追加日志到 `output/cron.log`(`cd` 后 `config.py` 才能就近找到 `.env`)。设计为**每分钟**触发:业务数据每次都刷新,天气靠缓存、用量读本地文件,避免高频打接口(见 `data.py`)。
|
||||
|
||||
## 架构(数据 → 渲染 → 推送 三层)
|
||||
|
||||
`main.py` 串联三层,各层职责单一、低耦合:
|
||||
|
||||
- **`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`。
|
||||
- **`data.py`** — 唯一数据入口 `get_dashboard_data()`,返回固定结构的 dict:`date`/`weather`/`mao`/`usage`。日期时间用真实系统时间;橘喵今日经营由 `_mao()` 经 `jm_api`、天气由 `_weather()` 经 `weather_api`(和风,定位南京·江宁)、Claude 用量由 `_usage()` 经 `usage_local`,**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可(renderer 依赖其 key:`weather.icon` 为和风图标代码;`mao.cols[].hb/tb` 为环比/同比 `(direction, text)`,direction 为 `None` 时不画三角;`usage.bars[]` 为 `{k, pct(或 None), time_pct?}`、`usage.warn` 为转红阈值)。注:`_mao` 仍会拉 `mao.trend`,但趋势图当前隐藏、renderer 不使用它。
|
||||
- **缓存**(为 cron 每分钟独立进程而设,必须落盘):天气 `.weather_cache.json`(TTL=`WEATHER_TTL`,默认 15min)、趋势 `.trend_cache.json`(按日期 key,每天只拉一次);两者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 落盘的本地文件(见 `usage_local`),失败即 `"--"`。两个缓存文件已在 `.gitignore`。
|
||||
- **用量的 pace(时间维度对比)**:`_usage()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)`、`pace=pct−time_pct`(>0 超前↑ / <0 节余↓);故即使 `utilization` 来自可能滞后的本地缓存,pace 仍随时钟准确推进。5h 窗口 18000s、7d 窗口 604800s。
|
||||
- **`jm_api.py`** — jm-devops 后端统计接口客户端,**设备密钥授权**(`grant_type=client_secret`,免验证码)。先用 `CLIENT_ID + CLIENT_SECRET` 调 `POST /auth/login` 换 token(进程内缓存、按 `expire_in` 提前刷新),再带 `Authorization: Bearer <token>` + `clientid` 头调 `/system/statistics/{realtime,recent-days}`;遇 401 清 token 重登一次重试。token 字段为蛇形 `access_token`/`expire_in`。注:历史上曾免鉴权直调、更早曾用账号密码+验证码 OCR 登录(ddddocr 不稳定),现统一为 client_secret 授权——密钥即长期凭证,务必走 HTTPS、优先用 `JM_CLIENT_SECRET` 环境变量、可在后端 `sys_client` 轮换。
|
||||
- **`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 `'RGB'` 模式、**仅黑/白/红三色**图;`render_to_file()` 落盘。布局硬编码为四区(render 内 A/B/C/D):A 大时钟+日期 / B 天气 / C 橘喵今日三列 / D 近30天趋势折线图。趋势线用 `_styled_polyline()` 按 solid/dashed/dotted 区分,各指标按自身极值独立归一化(故只看趋势形状、不可横向比绝对值)。
|
||||
- **红色点缀系统**(改配色从这里动,务必保持克制):模块顶部定义 `BLACK/WHITE/RED` 三元组,`RED=(255,0,0)` 是给设备红通道的明确信号。红只用在四处:① 定位水滴(`_loc_pin(fg=RED)`)② 天气图标的太阳与闪电(`_weather_icon(accent=RED)`,云/雨/雪/雾/月仍黑)③ 环比/同比**涨=红·跌=黑**——箭头与百分比数值一起着色(`_cmp_line`,「环比/同比」标签仍黑,红涨绿跌之「红涨」)④ 趋势图**主指标(流水)**折线+末点红点(按 `k=="流水"` 命中,图例同步红)。加红先问「是否承载语义」,避免红蔓延稀释注意力。
|
||||
- **`usage_local.py`** — Claude Code 用量本地读取,**不发任何网络请求**:只读 `config.USAGE_LOCAL_PATH`(默认 `/tmp/claude/statusline-usage-cache.json`,由已安装的 claude-statusline 落盘),取 `five_hour`/`seven_day` 的 `utilization`(已用%) 与 `resets_at`(重置时间)。注:新版 Claude Code 经 stdin 喂 statusline,该磁盘缓存刷新不频繁、**utilization 可能滞后**;但 `resets_at` 是绝对时间,故 `data._usage` 现算的 pace 始终准。文件缺失/损坏→用量区 `"--"`。(曾评估直调 Anthropic OAuth 用量接口 `/api/oauth/usage`,按需求改为只读本地。)
|
||||
- **`renderer.py`** — `DashboardRenderer.render(data)` 返回 PIL `'RGB'` 模式、**仅黑/白/红三色**图;`render_to_file()` 落盘。布局硬编码为**上中下三段**,两道 `hdash` 横规分隔(y=90 / 200):
|
||||
- **A 顶部**(0–90):大时钟(F48)+日期(左)/ 天气(右:红定位水滴 + 温度 + 天气·温区 + 全矢量图标)。
|
||||
- **B 中部主角 Claude Usage**(`_label` 标题 + 两条 `_usage_row`):每条单行 = 左标签 `5h`/`7d`(F13) + 粗进度条 + 百分比(F16) + 时间维度对比 pace(F13,`_pace`);`pct≥warn`→该条进度条(含边框)与百分比转红预警;pace 用上/下三角替代 +/−(超前↑ / 节余↓)。整行「标签·进度条·百分比·三角」用 `_mid()` 按墨迹竖直中心对齐到同一中线。
|
||||
- **C 底部 橘喵今日实时**(`_label` + 三列,版式对齐主分支):`■ 橘喵·今日实时` + `上次更新`,列内 名(F12)/值(F24 粗)/环比·同比(`_cmp_line`),相对标题偏移 +18/+34/+64/+82,底部留 ~11px 下边距。
|
||||
- **红色点缀系统**(改配色务必克制):模块顶部 `BLACK/WHITE/RED`,`RED=(255,0,0)` 是给设备红通道的明确信号。红只用在:① 定位水滴(`_loc_pin(fg=RED)`)② 天气太阳/闪电(`_weather_icon(accent=RED)`,云/雨/雪/雾/月仍黑)③ 用量进度条与百分比(`pct≥warn`) + pace 超前(↑) ④ 环比/同比「涨」的三角与数值(`_cmp_line`,跌/标签仍黑)。**配色铁律**:红与黑同属深色、红叠黑对比极低,故红只压白底或作大块实心(白字压红),**绝不红字压黑底**。加红先问「是否承载语义」。
|
||||
- **`_trend_chart`(当前隐藏)**:近30天三指标折线,用 `_styled_polyline()` 按 solid/dashed/dotted 区分、主指标(流水)红线,各指标按自身极值独立归一化。已保留但 `render()` 未调用,想恢复即在 C 前挂回。
|
||||
- **天气图标全矢量手绘**(无图片资源):`_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)。设备 ID 直接取 `config.DEVICE_ID`(即 `.env` 的 `ZECTRIX_DEVICE_ID` 硬件 MAC),`resolve_device_id()` 仅校验非空、未配置即报错——**已废弃获取设备列表再取第一个的逻辑**(原 `get_devices()` 已删除,不再调 `GET /devices`)。
|
||||
- **`config.py`** — 顶部 `_load_dotenv()` 先加载同目录 `.env`,再定义 API、设备、画布尺寸、字体路径、推送参数;凭证 fallback 为空串,真实值来自 `.env`/环境变量。
|
||||
@@ -50,7 +56,7 @@ ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 main.py
|
||||
|
||||
- **红只作语义强调**:方向(涨)、警示(闪电)、强调(定位/主折线);红占墨保持很小(<10% 墨迹),滥用会稀释注意力、破坏层级。整图**只允许三种纯色** `(0,0,0)/(255,255,255)/(255,0,0)`——渲染后可用 `img.getcolors()` 自查,出现第四种色(多为抗锯齿灰边)即为 bug。矢量图元(line/ellipse/polygon/arc)默认不抗锯齿故为纯色;文字靠 `fontmode="1"` 保持纯色。
|
||||
|
||||
- 字体固定用 **Fusion Pixel 12px 点阵字体**(`fonts/`,OFL 协议),只按整数倍尺寸 **12/24/48** 渲染(`F12/F24/F48`),并设 `d.fontmode = "1"` 关闭抗锯齿,否则像素会糊。
|
||||
- 字体固定用 **Fusion Pixel 12px 点阵字体**(`fonts/`,OFL 协议),首选整数倍尺寸 **12/24/48**(`F12/F24/F48`)最锐利;用量区为拿到合适的字号层级另用了 **F13/F16**(1.08x/1.33x 非整数倍,笔画略不均,属有意取舍),靠 `d.fontmode = "1"` 关抗锯齿仍保持纯色不糊。跨字号竖直居中统一用 `_mid()`(按 `getbbox` 墨迹中心对齐)。
|
||||
- 加粗用「伪粗体」:`_draw()` 按 1~bold px 水平偏移叠绘笔画(点阵字体只有单一字重)。
|
||||
- 推送时 `DITHER = False`(硬阈值),纯黑白图最锐利,勿改成 `true`。
|
||||
- 坐标、字号均为像素网格上的硬编码常量;调布局时注意各区分隔线 `hdash` 的 y 值与下方组件位置联动。
|
||||
|
||||
50
README.md
50
README.md
@@ -1,30 +1,32 @@
|
||||
# 墨水屏仪表盘推送(eink-push)
|
||||
|
||||
将橘喵今日经营、近30天趋势、日期与天气合成为一张 **400×300 三色(黑/白/红,BWR)**图片,
|
||||
将日期天气、Claude Code 用量、橘喵今日经营合成为一张 **400×300 三色(黑/白/红,BWR)**图片,
|
||||
推送到 Zectrix 极趣云墨水屏设备。
|
||||
|
||||
纯 Python + Pillow + Fusion Pixel 点阵字体,**无浏览器依赖**,适合后台/定时运行。
|
||||
|
||||
> **`feature/colorful` 分支**:新设备支持红色。红作为**稀缺强调色**只点缀四处——定位图标、
|
||||
> 天气的太阳/闪电、环比同比「涨」的三角与数值(红涨)、趋势图主指标(流水)折线——占墨极小,其余仍黑白。
|
||||
> **`feature/colorful` 分支**:新设备支持红色,渲染升级为黑/白/红三色,版式为上中下三段
|
||||
> (时钟天气 / Claude Usage / 今日实时)。红作**语义强调**——用量吃紧预警、涨、定位、天气太阳/闪电——
|
||||
> 全图只三种纯色、无灰阶不抖动。近30天趋势图当前隐藏(代码保留可挂回)。
|
||||
|
||||
## 效果
|
||||
|
||||
- 顶部:大号时钟 + 日期 | 天气(矢量手绘图标 + 温度 + 天气/温区)
|
||||
- 中部:橘喵今日实时经营(单量 / 流水 / 毛利,含环比、同比涨跌)
|
||||
- 底部:近30天三指标趋势折线图(同一图表,实线 / 虚线 / 点线区分,X 轴仅月/日)
|
||||
- 顶部:大号时钟 + 日期 | 天气(矢量手绘图标 + 温度 + 天气/温区,定位南京·江宁)
|
||||
- 中部:**Claude Usage**——5h / 7d 用量进度条 + 大号百分比 + 时间维度对比 pace(用量%−时间%,超前↑ / 节余↓);用量 ≥ 阈值时该条转红预警
|
||||
- 底部:橘喵今日实时经营(单量 / 流水 / 毛利,含环比、同比涨跌,涨=红)
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
eink-push/
|
||||
├── main.py # 入口:取数据 → 渲染 → 推送
|
||||
├── config.py # 配置:API Key、设备、字体、尺寸、推送参数、缓存路径
|
||||
├── data.py # 数据层:拉真实数据并映射,失败回退占位 "--"(不回退 mock);含天气/趋势缓存
|
||||
├── jm_api.py # jm-devops 后端统计接口客户端(免登录 GET,仅带 clientid 头)
|
||||
├── config.py # 配置:设备、天气、用量、字体、尺寸、推送参数、缓存路径
|
||||
├── data.py # 数据层:拉真实数据并映射,失败回退占位 "--"(不回退 mock);天气/趋势缓存
|
||||
├── jm_api.py # jm-devops 后端统计接口客户端(设备密钥 client_secret 授权 + token 缓存)
|
||||
├── weather_api.py # 和风天气(QWeather)客户端(实时天气 + 今日温区)
|
||||
├── usage_local.py # Claude Code 用量:只读本地 statusline 缓存文件(不发网络请求)
|
||||
├── renderer.py # 渲染层:DashboardRenderer 生成 400×300 三色(黑/白/红)图片
|
||||
├── pusher.py # 推送层:设备列表 / 推送图片(标准库 urllib)
|
||||
├── pusher.py # 推送层:按 .env 设备 MAC 直接推送图片(标准库 urllib)
|
||||
├── run.sh # cron 包装脚本(本机专用,git 忽略)
|
||||
├── fonts/ # Fusion Pixel 点阵字体(OFL 协议)
|
||||
├── output/ # 生成的图片与 cron.log(git 忽略)
|
||||
@@ -41,7 +43,7 @@ pip install -r requirements.txt # 仅 Pillow>=10.0;网络请求用标准库
|
||||
## 使用
|
||||
|
||||
```bash
|
||||
python3 main.py # 渲染并推送(默认取设备列表第一个)
|
||||
python3 main.py # 渲染并推送(设备取 .env 的 ZECTRIX_DEVICE_ID)
|
||||
python3 main.py --render-only # 只渲染到 output/dashboard.png,不推送(本地调试渲染用)
|
||||
```
|
||||
|
||||
@@ -53,11 +55,13 @@ 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
|
||||
# Claude Code 用量(只读本地 statusline 缓存,不发请求)
|
||||
USAGE_LOCAL_PATH=/tmp/claude/statusline-usage-cache.json USAGE_WARN=80
|
||||
```
|
||||
|
||||
## 定时刷新(cron)
|
||||
|
||||
设计为**每分钟**触发:业务数据每次都刷新,天气/趋势靠缓存避免高频打接口(见下「设计说明」)。
|
||||
设计为**每分钟**触发:业务数据每次都刷新,天气靠缓存、用量读本地文件,避免高频打接口(见下「设计说明」)。
|
||||
`run.sh` 是 cron 入口——cron 不继承登录环境,故脚本内 python 写死绝对路径、`cd` 到项目目录后追加日志到 `output/cron.log`。
|
||||
|
||||
```cron
|
||||
@@ -68,13 +72,14 @@ QWEATHER_HOST=... QWEATHER_KEY=... WEATHER_LOCATION=经度,纬度 WEATHER_TTL
|
||||
|
||||
## 数据来源
|
||||
|
||||
- **橘喵今日经营 + 近30天趋势**:接入 jm-devops 后端统计接口(`jm_api.py`),
|
||||
免登录 GET(仅带 `clientid` 头、无 token):
|
||||
- **橘喵今日经营**:接入 jm-devops 后端统计接口(`jm_api.py`,设备密钥 client_secret 授权):
|
||||
- `GET /system/statistics/realtime` —— 今日单量/流水/毛利 + 环比/同比
|
||||
- `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`(控制台-设置查看)。
|
||||
- `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`。
|
||||
- **Claude Code 用量**:**只读本地**(`usage_local.py`,不发请求)——读 claude-statusline 落盘的
|
||||
`/tmp/claude/statusline-usage-cache.json`,取 5h/7d 的 `utilization` 与 `resets_at`。
|
||||
注:新版 Claude Code 经 stdin 喂 statusline,该缓存 `utilization` 可能滞后;`resets_at` 为绝对时间,故 pace 时间对比始终准确。
|
||||
- **日期 / 时间 / 上次更新**:真实系统时间。
|
||||
|
||||
> `data.py` 负责把响应映射成渲染结构;**任何接口失败都回退占位 `"--"`,不回退 mock**。
|
||||
@@ -86,15 +91,16 @@ QWEATHER_HOST=... QWEATHER_KEY=... WEATHER_LOCATION=经度,纬度 WEATHER_TTL
|
||||
- **天气缓存** `.weather_cache.json`:TTL = `WEATHER_TTL`(默认 15 分钟),未过期不发请求。
|
||||
- **趋势缓存** `.trend_cache.json`:按日期为 key,当天命中即不再调 `recent-days`,每天只拉一次。
|
||||
|
||||
两者拉取失败均**沿用旧缓存**避免趋势/天气区整片 `"--"`,仍无缓存才回退占位。想强制重拉删掉对应缓存文件即可(均已 git 忽略)。
|
||||
两者拉取失败均**沿用旧缓存**避免天气区整片 `"--"`,仍无缓存才回退占位。想强制重拉删掉对应缓存文件即可(均已 git 忽略)。
|
||||
Claude 用量**不自建缓存**——直接读 statusline 落盘的本地文件,文件缺失/损坏即显示 `"--"`。
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 设备为**三色(黑/白/红)无真灰阶**,灰阶只能抖动成网点(难看),故底色只用黑白 + 红作稀缺强调,靠构图、字号、留白建立层级。
|
||||
- **红只承载语义**(方向·警示·强调),占墨保持很小(实测红≈全图 0.6%、占墨迹 <10%);整图仅三种纯色 `(0,0,0)/(255,255,255)/(255,0,0)`,无灰、不抖动。
|
||||
- 字体用 **Fusion Pixel 12px 点阵字体**,按整数倍尺寸(12/24/48)渲染、关闭抗锯齿 → 像素锐利;
|
||||
重点数据用「伪粗体」(偏移叠绘)加粗。
|
||||
- 字体用 **Fusion Pixel 12px 点阵字体**,首选整数倍(12/24/48)最锐利;用量区另用 13/16px(非整数倍、笔画略不均)取字号层级,关抗锯齿仍保持纯色不糊;重点数据用「伪粗体」(偏移叠绘)加粗。
|
||||
- **Claude Usage**:每条单行 = 左标签(5h/7d) + 粗进度条 + 百分比 + pace(上/下三角);用量 ≥ `USAGE_WARN`(默认 80) 时该条进度条与百分比转红预警。行内所有元素按墨迹竖直中心对齐到同一中线。
|
||||
- 天气图标为**全矢量手绘**(无图片资源):按和风 icon 代码归并为 8 类(晴/晴夜/多云/阴/雷/雨/雪/雾),用基本图元描边画出;太阳与闪电着红。
|
||||
- 趋势折线各指标按自身极值独立归一化,故只表达**趋势形状**,不可横向比绝对值;主指标(流水)着红领读、末点标红点。
|
||||
- 近30天趋势折线图当前**隐藏**(`renderer._trend_chart` 及依赖保留、未调用,随时可挂回)。
|
||||
- 推送用 `dither=false`(硬阈值),三色纯色图最锐利。
|
||||
```
|
||||
13
config.py
13
config.py
@@ -62,11 +62,18 @@ TENANT_ID = os.environ.get("JM_TENANT_ID", "000000")
|
||||
# 未配置时天气区显示占位 "--",不影响其余渲染与推送。
|
||||
QWEATHER_HOST = os.environ.get("QWEATHER_HOST", "") # 用户专属 API Host
|
||||
QWEATHER_KEY = os.environ.get("QWEATHER_KEY", "")
|
||||
# 定位固定为南京·秦淮区。和风经纬度格式为「经度,纬度」
|
||||
WEATHER_LOCATION = os.environ.get("WEATHER_LOCATION", "118.788,32.014")
|
||||
WEATHER_LOC_NAME = "南京·秦淮"
|
||||
# 定位固定为南京·江宁区。和风经纬度格式为「经度,纬度」
|
||||
WEATHER_LOCATION = os.environ.get("WEATHER_LOCATION", "118.840,31.953")
|
||||
WEATHER_LOC_NAME = "南京·江宁"
|
||||
# 天气缓存:业务数据每分钟刷新,但天气 15 分钟才重新拉一次(cron 每次独立进程,缓存须落盘)。
|
||||
WEATHER_TTL = int(os.environ.get("WEATHER_TTL", "900")) # 秒,默认 900=15 分钟
|
||||
WEATHER_CACHE_PATH = os.path.join(BASE_DIR, ".weather_cache.json")
|
||||
# 近30天趋势缓存:recent-days 每天只更新一次,按日期为 key 缓存,当天命中即不再请求。
|
||||
TREND_CACHE_PATH = os.path.join(BASE_DIR, ".trend_cache.json")
|
||||
|
||||
# ---- Claude Code 用量(Claude Usage 模块)数据源 ----
|
||||
# 只读本地:读取 claude-statusline 落盘的用量缓存文件,**不发任何网络请求**(详见 usage_local.py)。
|
||||
# 注意:新版 Claude Code 经 stdin 喂 statusline,该磁盘缓存刷新不频繁、utilization 可能滞后;
|
||||
# resets_at 为绝对时间,故 pace(时间维度对比)始终准确。文件缺失时用量区显示 "--"。
|
||||
USAGE_LOCAL_PATH = os.environ.get("USAGE_LOCAL_PATH", "/tmp/claude/statusline-usage-cache.json")
|
||||
USAGE_WARN = int(os.environ.get("USAGE_WARN", "80")) # 用量 ≥ 此百分比 → 进度条与百分比转红预警
|
||||
|
||||
53
data.py
53
data.py
@@ -13,10 +13,12 @@ from datetime import datetime
|
||||
import config
|
||||
import jm_api
|
||||
import weather_api
|
||||
import usage_local
|
||||
|
||||
WEEK_CN = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
|
||||
TREND_DAYS = 30 # 近 N 天趋势
|
||||
FIVE_HOUR, SEVEN_DAY = 5 * 3600, 7 * 86400 # 两个滚动窗口长度(秒),用于算「时间已流逝%」
|
||||
|
||||
|
||||
def _read_weather_cache():
|
||||
@@ -166,6 +168,56 @@ def _mao(now):
|
||||
return {"upd": upd, "cols": cols, "trend": trend}
|
||||
|
||||
|
||||
# ---------- Claude Code 用量 ----------
|
||||
def _parse_iso_ts(s):
|
||||
"""ISO 时间串 → epoch 秒;兼容结尾 'Z'。失败返回 None。"""
|
||||
if not s:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _window_time_pct(resets_at, window, now):
|
||||
"""该滚动窗口「已流逝时间百分比」= (window − 距重置剩余) / window ×100。失败返回 None。"""
|
||||
reset = _parse_iso_ts(resets_at)
|
||||
if reset is None:
|
||||
return None
|
||||
remaining = max(0, min(window, reset - now.timestamp()))
|
||||
return int(round((window - remaining) / window * 100))
|
||||
|
||||
|
||||
def _usage(now):
|
||||
"""Claude Code 用量(5h / 7d)——只读本地 statusline 缓存(见 usage_local.py),**不发任何请求**。
|
||||
|
||||
time_pct(时间已流逝%) 与 pace 每次按当前时间**现算**(resets_at 为绝对时间,始终准确);
|
||||
utilization 取自本地缓存、可能滞后。文件缺失/损坏 → 回退占位 pct=None(renderer 显示 "--")。
|
||||
结构契约(renderer 依赖):{title, warn, bars:[{k, pct(0~100 或 None), time_pct(0~100)?}]};
|
||||
pct ≥ warn 时该条进度条与百分比转红;pace = pct − time_pct(>0 超前↑ / <0 节余↓)。
|
||||
"""
|
||||
wins = {}
|
||||
try:
|
||||
raw = usage_local.get_usage()
|
||||
for key, label, win in (("five_hour", "5h", FIVE_HOUR), ("seven_day", "7d", SEVEN_DAY)):
|
||||
seg = raw.get(key) or {}
|
||||
wins[label] = {"pct": int(round(float(seg.get("utilization", 0)))),
|
||||
"resets_at": seg.get("resets_at"), "win": win}
|
||||
except Exception as e:
|
||||
print(f"[警告] Claude 用量读取失败:{e}")
|
||||
|
||||
bars = []
|
||||
for label in ("5h", "7d"):
|
||||
w = wins.get(label) or {}
|
||||
pct = w.get("pct")
|
||||
bar = {"k": label, "pct": pct}
|
||||
tp = _window_time_pct(w.get("resets_at"), w.get("win"), now)
|
||||
if pct is not None and tp is not None:
|
||||
bar["time_pct"] = tp
|
||||
bars.append(bar)
|
||||
return {"title": "Claude Usage", "warn": config.USAGE_WARN, "bars": bars}
|
||||
|
||||
|
||||
def get_dashboard_data(now=None):
|
||||
"""返回渲染所需的完整数据字典。"""
|
||||
now = now or datetime.now()
|
||||
@@ -177,4 +229,5 @@ def get_dashboard_data(now=None):
|
||||
},
|
||||
"weather": _weather(now),
|
||||
"mao": _mao(now),
|
||||
"usage": _usage(now),
|
||||
}
|
||||
|
||||
138
renderer.py
138
renderer.py
@@ -4,12 +4,20 @@
|
||||
渲染层 —— 将数据绘制成 400x300 三色(黑/白/红)墨水屏仪表盘。
|
||||
|
||||
设计要点:
|
||||
- 三色 BWR:仍无真灰阶(灰阶只能抖动成网点,弃用),底色黑白;红作为稀缺强调色点缀。
|
||||
红只承载语义——方向(涨)、警示、寻路锚点——占墨极小,靠构图 + 字号 + 留白建立层级。
|
||||
- 三色 BWR:仍无真灰阶(灰阶只能抖动成网点,弃用);底色黑白,靠留白 + 大字号 + 一处大红块建层级。
|
||||
克制而有分量:全图只一个「大红」焦点(用量超阈值的整条红进度条),其余红为小面积语义点缀。
|
||||
- 关键墨水屏配色铁律:红与黑同属「深色」,红叠黑对比极低 → 红只出现在**白底**(红字/红三角/红块),
|
||||
**绝不红字压黑底**;白底红块 + 白字最稳。
|
||||
- Fusion Pixel 点阵字体,按整数倍尺寸(12/24/48)渲染,fontmode='1' 关闭抗锯齿 → 像素锐利。
|
||||
- 重点数据用「伪粗体」(偏移叠绘)加粗,仍对齐像素网格。
|
||||
布局:上=大时钟+日期 / 天气;中=橘喵今日经营三列;下=近30天三指标趋势折线图。
|
||||
红点缀清单:时钟冒号 / 定位图标 / 天气图标的太阳与闪电 / 两处标题方块■ / 环比同比“涨”三角 / 趋势图主指标(单量)折线。
|
||||
布局(自上而下三段,两道横规分隔):
|
||||
A 顶部:大时钟+日期(左)/ 天气(右),白底黑字 + 红点缀(定位红水滴、太阳/闪电红);
|
||||
B 中部主角:Claude Usage——每条单行 = 左标签(5h/7d,F13) + 进度条 + 百分比(F16,主数字) + 时间维度对比 pace(F13);
|
||||
pct ≥ 阈值(warn) 时该条进度条(含边框)与百分比整体转红;
|
||||
pace = 用量% − 时间% ,用上/下三角(替代 +/−)表示超前/节余,超前(↑)红、节余(↓)黑;
|
||||
C 底部:橘喵今日实时三列,环比/同比「涨=红·跌=黑」。
|
||||
近30天趋势图暂隐藏(`_trend_chart` 及其依赖 `_styled_polyline`/`_style_sample` 保留、未调用,随时可再挂回)。
|
||||
红出现处:① 定位水滴 ② 天气太阳/闪电 ③ 用量进度条与百分比(≥warn) ④ pace 超前(↑) ⑤ 环比/同比「涨」的三角与数值。
|
||||
"""
|
||||
import math
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
@@ -26,6 +34,8 @@ class DashboardRenderer:
|
||||
def __init__(self, font_path=config.FONT_PATH, size=(config.WIDTH, config.HEIGHT)):
|
||||
self.W, self.H = size
|
||||
self.F12 = ImageFont.truetype(font_path, 12)
|
||||
self.F13 = ImageFont.truetype(font_path, 13) # 5h/7d 与 pace 用;接近原生 12px,畸变很小
|
||||
self.F16 = ImageFont.truetype(font_path, 16) # 非整数倍(1.33x):靠 fontmode='1' 保持纯色,笔画略不均可接受
|
||||
self.F24 = ImageFont.truetype(font_path, 24)
|
||||
self.F48 = ImageFont.truetype(font_path, 48)
|
||||
|
||||
@@ -83,12 +93,66 @@ class DashboardRenderer:
|
||||
self.tri(x + trw / 2, y + 8.5, 7, up=up, fill=val_color); x += trw + tgap
|
||||
self.text(x, y, val, self.F12, fill=val_color)
|
||||
|
||||
def _title_bar(self, y, title, upd):
|
||||
"""标题栏:■ 标题(左) + 上次更新 HH:mm(右)。"""
|
||||
sq, sq_y = 6, y + 4
|
||||
self.d.rectangle([10, sq_y, 10 + sq, sq_y + sq], fill=BLACK)
|
||||
self.text(10 + sq + 6, y, title, self.F12)
|
||||
self.rtext(self.W - 10, y, f"上次更新 {upd}", self.F12)
|
||||
def _label(self, y, title, right=None):
|
||||
"""区块标题(两区块统一,对齐主分支 _title_bar 风格):
|
||||
■ 方块装饰(8px,与 F12 标题墨迹竖直居中) + 标题(F12,常规字重) + 可选右侧信息。"""
|
||||
SQ = 8
|
||||
self.d.rectangle([10, y + 4, 10 + SQ, y + 4 + SQ], fill=BLACK) # 中心≈y+8,对齐 F12 墨迹中心(≈y+8.5)
|
||||
self.text(10 + SQ + 6, y, title, self.F12)
|
||||
if right:
|
||||
self.rtext(self.W - 10, y, right, self.F12)
|
||||
|
||||
def _mid(self, x, cy, s, ft, fill=BLACK, bold=0, align="l"):
|
||||
"""按字符串「墨迹竖直中心」对齐到 cy 绘制——跨字号(F12/F24)也能真正居中,
|
||||
避免不同字号按顶部对齐时的视觉错位。align: l 左对齐 / r 右对齐 / c 居中于 x。"""
|
||||
_, t, _, b = ft.getbbox(s) # 该串墨迹在字身框内的上/下沿
|
||||
y = cy - (t + b) / 2.0
|
||||
w = self._tw(s, ft)
|
||||
if align == "r":
|
||||
x -= w + bold
|
||||
elif align == "c":
|
||||
x -= (w + bold) / 2.0
|
||||
self._draw(x, y, s, ft, fill, bold)
|
||||
|
||||
def _pace(self, xr, cy, diff):
|
||||
"""时间维度对比(pace = 用量% − 时间%):>0 超前↑红;<0 节余↓黑;=0 持平。
|
||||
数值(F13)两位补零、右端对齐 xr,小三角(6)紧贴其左;均竖直居中 cy。返回整体宽度供左侧定位。"""
|
||||
up = diff > 0
|
||||
color = RED if up else BLACK
|
||||
txt = f"{abs(int(diff)):02d}"
|
||||
tw = self._tw(txt, self.F13)
|
||||
self._mid(xr, cy, txt, self.F13, fill=color, bold=1, align="r")
|
||||
if diff == 0:
|
||||
return tw
|
||||
TRI_GAP, TRI = 4, 6
|
||||
self.tri(xr - tw - TRI_GAP - TRI / 2, cy, TRI, up=up, fill=color)
|
||||
return tw + TRI_GAP + TRI
|
||||
|
||||
def _usage_row(self, x0, x1, y, bar, warn):
|
||||
"""一条用量(单行):左标签(5h/7d, F13) + 进度条 + 百分比(F16, 主数字) + 时间维度对比 pace(F13)。
|
||||
右侧从右往左排布,显式控制两处横向间距:G1 进度条↔百分比、G2 百分比↔pace;全部竖直居中 cy。
|
||||
pct ≥ warn → 进度条(含整条边框)与百分比转红;边框随条色,红条即红框,无黑白错位。"""
|
||||
raw = bar.get("pct")
|
||||
ok = isinstance(raw, (int, float)) # None/缺失 → 显示 "--"
|
||||
pct = max(0, min(100, int(raw))) if ok else 0
|
||||
color = RED if (ok and pct >= warn) else BLACK
|
||||
LABEL_W, G1, G2, H = 26, 13, 8, 15 # G1 进度条↔百分比、G2 百分比↔pace(视觉约 12/11)
|
||||
cy = y + 10 # 行竖直中心
|
||||
self._mid(x0, cy, bar["k"], self.F13, bold=1) # 左标签 F13
|
||||
# 右侧从右往左:pace(最右) → 百分比 → 进度条
|
||||
pct_r = x1
|
||||
if ok and "time_pct" in bar:
|
||||
pace_w = self._pace(x1, cy, pct - int(bar["time_pct"]))
|
||||
pct_r = x1 - pace_w - G2
|
||||
pct_txt = f"{pct}%" if ok else "--"
|
||||
self._mid(pct_r, cy, pct_txt, self.F16, fill=color, bold=1, align="r") # 百分比 F16(主)
|
||||
bx0 = x0 + LABEL_W
|
||||
bx1 = pct_r - self._tw(pct_txt, self.F16) - G1
|
||||
by = cy - H // 2
|
||||
self.d.rectangle([bx0, by, bx1, by + H], outline=color, width=1) # 轨道边框随条色
|
||||
fw = (bx1 - bx0) * pct / 100.0
|
||||
if fw >= 2:
|
||||
self.d.rectangle([bx0, by, bx0 + fw, by + H], fill=color)
|
||||
|
||||
def _loc_pin(self, x, y, w=8, fg=BLACK, bg=WHITE):
|
||||
"""定位图标:实心水滴(圆头+尖尾) + 白色挖孔,1-bit 下轮廓清晰。
|
||||
@@ -285,55 +349,63 @@ class DashboardRenderer:
|
||||
|
||||
# ---------- 入口 ----------
|
||||
def render(self, data):
|
||||
"""根据 data 渲染并返回 PIL.Image('RGB' 模式,仅黑/白/红三色)。"""
|
||||
"""根据 data 渲染并返回 PIL.Image('RGB' 模式,仅黑/白/红三色)。
|
||||
|
||||
版式(自上而下三段,两道横规分隔):
|
||||
A 顶部:大时钟+日期(左)/ 天气(右)——白底黑字,红点缀(定位/太阳闪电);
|
||||
B 中部主角:Claude Code 用量——每条=标题行 + 一条粗进度条,pct≥阈值转红预警;
|
||||
C 底部:橘喵今日实时三列(涨=红·跌=黑)。近30天趋势图暂隐藏(_trend_chart 保留)。
|
||||
"""
|
||||
self.img = Image.new("RGB", (self.W, self.H), WHITE)
|
||||
self.d = ImageDraw.Draw(self.img)
|
||||
self.d.fontmode = "1" # 关闭抗锯齿,点阵锐利(RGB 上仍生效,纯色描边不发糊)
|
||||
|
||||
dt, wt, mao = data["date"], data["weather"], data["mao"]
|
||||
dt, wt, mao, usage = data["date"], data["weather"], data["mao"], data["usage"]
|
||||
|
||||
# A. Hero:大时钟 + 日期(时钟上移使「顶部留白 ≈ 与日期间距」对称)
|
||||
# ---- A. 顶部:大时钟+日期(左)/ 天气(右)。白底黑字,红点缀 ----
|
||||
self.text(10, 0, dt["time"], self.F48, bold=2)
|
||||
self.text(12, 64, f'{dt["week"]} {dt["greg"]}', self.F12)
|
||||
|
||||
# B. 天气:右对齐文字 + 居中的 28° + 放大右靠的图标
|
||||
DIV_Y = 90 # 天气区底部分割线
|
||||
DIV_Y = 90
|
||||
loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]} {wt["range"]}'
|
||||
self.rtext(390, 8, loc, self.F12)
|
||||
self.rtext(390, 30, temp, self.F24, bold=1)
|
||||
self.rtext(390, 64, cr, self.F12)
|
||||
# 定位图标:置于 loc 文字左侧,留间距并与文字竖直居中
|
||||
PIN_W, PIN_GAP = 8, 6
|
||||
loc_pin_x = 390 - self._tw(loc, self.F12) - PIN_GAP - PIN_W
|
||||
self._loc_pin(loc_pin_x, 10, PIN_W, fg=RED) # 定位图标:红水滴,地图别针的通用语汇
|
||||
self._loc_pin(loc_pin_x, 10, PIN_W, fg=RED) # 定位红水滴
|
||||
s = 1.5
|
||||
iw, ih = 36 * s, 38 * s # 图标原生约 36x38
|
||||
text_left = min(loc_pin_x,
|
||||
390 - self._tw(temp, self.F24) - 1,
|
||||
390 - self._tw(cr, self.F12))
|
||||
icon_y = (DIV_Y - ih) / 2 # 顶部(0)与分割线正中:上下留白相等
|
||||
self._weather_icon(text_left - 12 - iw, icon_y, self._weather_category(wt.get("icon")), s=s, accent=RED)
|
||||
|
||||
self._weather_icon(text_left - 12 - iw, (DIV_Y - ih) / 2,
|
||||
self._weather_category(wt.get("icon")), s=s, accent=RED)
|
||||
self.hdash(8, 392, DIV_Y)
|
||||
|
||||
# C. 橘喵今日经营:三列(数值 + 环比/同比)
|
||||
self._title_bar(96, "橘喵·今日实时", mao["upd"])
|
||||
# ---- B. 中部主角:Claude Usage(单行:标签 + 进度条 + 百分比 + pace,≥阈值转红)----
|
||||
# 标题贴近上分割线(与今日实时一致的 6px 间距);两条进度条在余下空间内等距(≈19px)
|
||||
self._label(96, usage.get("title", "Claude Usage"))
|
||||
warn = usage.get("warn", 80)
|
||||
ry = 123
|
||||
for b in usage.get("bars", []):
|
||||
self._usage_row(10, 390, ry, b, warn)
|
||||
ry += 31 # 略压缩两条间距,给底部让出下边距
|
||||
|
||||
self.hdash(8, 392, 187) # B/C 分割线上移,最终给底部今日实时留出与整图下边的间距
|
||||
|
||||
# ---- C. 底部:橘喵·今日实时(三列,版式对齐主分支:标题+18=名/+34=值/+64=环比/+82=同比)----
|
||||
self._label(193, "橘喵·今日实时", right=f'上次更新 {mao["upd"]}')
|
||||
cols = mao["cols"]; n = len(cols)
|
||||
ax0, ax1 = 8, 392
|
||||
cw = (ax1 - ax0) / n
|
||||
for i in range(1, n):
|
||||
self.vdash(ax0 + cw * i, 128, 192)
|
||||
self.vdash(ax0 + cw * i, 225, 289)
|
||||
for i, c in enumerate(cols):
|
||||
cx = ax0 + cw * (i + 0.5)
|
||||
self.ctext(cx, 114, c["k"], self.F12)
|
||||
self.ctext(cx, 130, c["v"], self.F24, bold=1)
|
||||
self._cmp_line(cx, 160, "环比", c["hb"])
|
||||
self._cmp_line(cx, 178, "同比", c["tb"])
|
||||
|
||||
self.hdash(8, 392, 200)
|
||||
|
||||
# D. 近30天三指标趋势折线图
|
||||
self._trend_chart(204, mao["trend"])
|
||||
self.ctext(cx, 211, c["k"], self.F12) # 指标名(标题+18)
|
||||
self.ctext(cx, 227, c["v"], self.F24, bold=1) # 数值 伪粗体(+34)
|
||||
self._cmp_line(cx, 257, "环比", c["hb"]) # +64
|
||||
self._cmp_line(cx, 275, "同比", c["tb"]) # +82,墨迹止于≈289,底部留≈10px
|
||||
|
||||
return self.img
|
||||
|
||||
|
||||
31
usage_local.py
Normal file
31
usage_local.py
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Claude Code 用量本地读取 —— 只读 claude-statusline 落盘的用量缓存,**不发任何网络请求**。
|
||||
|
||||
数据文件:config.USAGE_LOCAL_PATH(默认 /tmp/claude/statusline-usage-cache.json),
|
||||
由已安装的 claude-statusline 写入。结构含 five_hour / seven_day,各有
|
||||
utilization(已用百分比) 与 resets_at(ISO 重置时间,供上层算「时间已流逝%」做 pace 对比)。
|
||||
|
||||
注意:新版 Claude Code 经 stdin 把额度喂给 statusline,该磁盘缓存刷新不频繁、utilization 可能滞后;
|
||||
resets_at 是绝对时间,故上层的 pace(时间维度对比)始终准确。文件缺失/损坏时抛错 → 上层回退占位 "--"。
|
||||
"""
|
||||
import json
|
||||
|
||||
import config
|
||||
|
||||
|
||||
def get_usage() -> dict:
|
||||
"""读取本地 statusline 用量缓存并返回(含 five_hour / seven_day)。缺失/损坏时抛出。"""
|
||||
with open(config.USAGE_LOCAL_PATH, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if "five_hour" not in data:
|
||||
raise RuntimeError(f"用量缓存缺 five_hour 字段:{config.USAGE_LOCAL_PATH}")
|
||||
return data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
d = get_usage()
|
||||
for k in ("five_hour", "seven_day"):
|
||||
s = d.get(k) or {}
|
||||
print(k, "util=", s.get("utilization"), "resets_at=", s.get("resets_at"))
|
||||
Reference in New Issue
Block a user