- 新增 SenseCraft 设备遥测客户端(api-key 走 .env),读电量/充电/温度/湿度,DEVICE_TTL 与设备 300s 上报周期同步 - 大时钟 F84 右侧一列三行左缘对齐:电池(充电绿/≤20% 红)、温度计+蓝色水滴、星期日期(与时钟基线对齐) - server 过期上游由串行改并行,整体最多等 FETCH_BUDGET 秒(默认 5);超预算的一路本次用旧值、后台拉完回填缓存,同一路不重复实拉 - 此前串行且上游 socket 超时 30s,任一路卡住即整图拖到平台抓取超时;「多路 TTL 同时到期」猜想经日志核查不成立,排查顺序写入 CLAUDE.md Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
154 lines
12 KiB
Python
154 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""全局配置。敏感项从同目录 .env 读取,真实环境变量(如 cron 注入)优先。"""
|
||
import os
|
||
|
||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
|
||
def _load_dotenv(path):
|
||
"""极简零依赖 .env 加载器:把 KEY=VALUE 写入 os.environ。
|
||
已存在的真实环境变量优先(不覆盖),故 cron/命令行注入仍可压过 .env。
|
||
支持 # 注释、空行、行内 KEY=VALUE,值两侧成对的单/双引号会被去掉。"""
|
||
try:
|
||
with open(path, "r", encoding="utf-8") as fp:
|
||
for raw in fp:
|
||
line = raw.strip()
|
||
if not line or line.startswith("#") or "=" not in line:
|
||
continue
|
||
key, _, val = line.partition("=")
|
||
key, val = key.strip(), val.strip()
|
||
if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'":
|
||
val = val[1:-1]
|
||
if key:
|
||
os.environ.setdefault(key, val)
|
||
except FileNotFoundError:
|
||
pass
|
||
|
||
|
||
_load_dotenv(os.path.join(BASE_DIR, ".env"))
|
||
|
||
# ---- 局域网墨水屏设备(喵喵固件,开源:https://gitee.com/gxp666111/miaomiao)----
|
||
# 不再走 Zectrix 公网 API + 设备 MAC,改为直连局域网设备 mDNS host 的 HTTP 接口。
|
||
EPD_HOST = os.environ.get("EPD_HOST", "epd400a44.local")
|
||
|
||
# ---- 时区 ----
|
||
# 服务端可能部署在境外 VPS,所有「现在时刻」与时间戳格式化统一按此时区(data.TZ),本机与服务端结果一致。
|
||
TZ_NAME = os.environ.get("TZ_NAME", "Asia/Shanghai")
|
||
|
||
# ---- 画布(喵喵固件 400x300 三色屏)----
|
||
WIDTH, HEIGHT = 400, 300
|
||
|
||
# ---- reTerminal E1002(800x480,E Ink Spectra 6 六色;经 SenseCraft HMI 的 HTML 控件云端抓取)----
|
||
# 产物:固定 800x480 的 index.html + 同目录 dashboard.png,需部署到公网 HTTPS 地址供平台每周期重新拉取。
|
||
E1002_WIDTH, E1002_HEIGHT = 800, 480
|
||
E1002_COLOR_STRIP = os.environ.get("E1002_COLOR_STRIP", "0") == "1" # 底部六色测试色条(真机校准用,默认关)
|
||
|
||
# ---- 图片服务(server.py:恒定 URL,每次请求内存中现渲染 800x480 PNG,供 SenseCraft HTML 控件周期抓取)----
|
||
# 服务端三路上游的内存缓存有效期(秒):收到请求先查缓存是否过期,过期才实拉并更新时间戳,否则直接用缓存渲染;
|
||
# 拉取失败沿用旧值且不更新时间戳(下次请求再试)。设 0 = 每次请求都实拉。天气/BTC 复用上面的 WEATHER_TTL/BTC_TTL。
|
||
MAO_TTL = int(os.environ.get("MAO_TTL", "60")) # 橘喵今日实时,默认 60 秒
|
||
# server.py 一次请求里等待过期上游并行实拉的总预算(秒):超预算的那路本次用上一次成功值,后台继续拉完更新缓存。
|
||
# 上游 socket 超时各 30s(和风/橘喵)、8s(BTC),若不设预算、任一路卡住会拖到平台抓取超时;预算 ≥ 正常三路并行耗时(~1s)即可。
|
||
FETCH_BUDGET = float(os.environ.get("FETCH_BUDGET", "5"))
|
||
# 真机验证结论:Image 控件只在填写 URL 时抓一次快照、此后不再更新;HTML 控件填同一图片 URL 则每周期重抓。
|
||
SERVER_HOST = os.environ.get("SERVER_HOST", "0.0.0.0")
|
||
SERVER_PORT = int(os.environ.get("SERVER_PORT", "8790"))
|
||
IMAGE_PATH = os.environ.get("IMAGE_PATH", "/e1002.png") # 方案一:恒定图片路径(每次请求内存渲染 PNG);可改成难猜的路径防扫描
|
||
HTML_PATH = os.environ.get("HTML_PATH", "/e1002.html") # 方案二:恒定网页路径(每次请求生成 800x480 现代网页,平台截图)
|
||
# 方案三:服务端用无头 Chrome 把方案二的网页截成 800x480,再按「最近色、不抖动」固化为六色 PNG(需服务器装 Chrome/Chromium)
|
||
HTML_PNG_PATH = os.environ.get("HTML_PNG_PATH", "/e1002-web.png")
|
||
CHROME_BIN = os.environ.get("CHROME_BIN", "") # 为空则按常见路径自动探测(macOS Google Chrome / google-chrome / chromium)
|
||
CHROME_EXTRA_ARGS = os.environ.get("CHROME_EXTRA_ARGS", "") # 例如 Linux 以 root 运行需 "--no-sandbox"
|
||
# 每通道二值化阈值(0~255):低于阈值归 0、否则归 255;取 160(而非 128)偏向保留墨迹,细笔画的浅灰边不至于被丢成白
|
||
HTML_PNG_THRESHOLD = int(os.environ.get("HTML_PNG_THRESHOLD", "160"))
|
||
PUSH_TOKEN = os.environ.get("PUSH_TOKEN", "") # 推送鉴权令牌(X-Push-Token 头);为空则拒绝一切推送
|
||
PUSH_STATE_PATH = os.environ.get("PUSH_STATE_PATH", "") # 推送数据落盘路径(重启后恢复);为空=仅内存
|
||
# 本机推送端(main.py --target push):服务地址(含协议与端口,不含路径),令牌同 PUSH_TOKEN
|
||
PUSH_URL = os.environ.get("PUSH_URL", "")
|
||
|
||
# ---- 资源路径 ----
|
||
FONT_PATH = os.path.join(BASE_DIR, "fonts", "fusion-pixel-12px-proportional-zh_hans.otf")
|
||
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
|
||
OUTPUT_PATH = os.path.join(OUTPUT_DIR, "dashboard.png")
|
||
E1002_OUTPUT_DIR = os.path.join(OUTPUT_DIR, "e1002")
|
||
E1002_PNG_PATH = os.path.join(E1002_OUTPUT_DIR, "dashboard.png")
|
||
E1002_HTML_PATH = os.path.join(E1002_OUTPUT_DIR, "index.html")
|
||
|
||
# ---- 橘喵 jm-devops 后端(今日经营 / 近30天趋势 数据源)----
|
||
# 鉴权方式:设备密钥授权(grant_type=client_secret,免验证码)。
|
||
# 用 CLIENT_ID + CLIENT_SECRET 调 /auth/login 换 token,再带 Bearer 调统计接口(详见 jm_api.py)。
|
||
# CLIENT_SECRET 等价于长期凭证,务必走 HTTPS、优先用环境变量覆盖、可在后端 sys_client 轮换。
|
||
BASE_API = os.environ.get("JM_BASE_API", "https://admin.jumiaotandian.com/prod-api")
|
||
CLIENT_ID = os.environ.get("JM_CLIENT_ID", "")
|
||
CLIENT_SECRET = os.environ.get("JM_CLIENT_SECRET", "")
|
||
GRANT_TYPE = os.environ.get("JM_GRANT_TYPE", "client_secret")
|
||
TENANT_ID = os.environ.get("JM_TENANT_ID", "000000")
|
||
|
||
# ---- 和风天气(QWeather)数据源 ----
|
||
# 专属 API Host 与 Key 在 https://console.qweather.com/setting 查看,须用环境变量配置;
|
||
# 未配置时天气区显示占位 "--",不影响其余渲染与推送。
|
||
QWEATHER_HOST = os.environ.get("QWEATHER_HOST", "") # 用户专属 API Host
|
||
QWEATHER_KEY = os.environ.get("QWEATHER_KEY", "")
|
||
# 定位固定为南京·江宁区。和风经纬度格式为「经度,纬度」
|
||
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 分钟(本机文件缓存与 server.py 内存缓存共用)
|
||
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")
|
||
|
||
# ---- 比特币 K 线(仅 800x480 宽版底部面板)----
|
||
# 免费公开接口,无需密钥,逐源兜底(OKX → Coinbase → Huobi,见 btc_api.py)。当前为时 K、近 48 小时(2 天);
|
||
# 每 5 分钟刷新(与 launchd 节律一致,最新一根随实时价推进)。拉取失败沿用旧缓存。缓存文件已在 .gitignore。
|
||
BTC_BAR = os.environ.get("BTC_BAR", "1H") # 周期:"1H" 时K / "1D" 日K
|
||
BTC_LIMIT = int(os.environ.get("BTC_LIMIT", "48")) # 展示最近 N 根(时 K 默认 48 = 近 2 天;2026-09-14 由 72 改 48)
|
||
BTC_TTL = int(os.environ.get("BTC_TTL", "300")) # 秒,默认 5 分钟(本机文件缓存与 server.py 内存缓存共用)
|
||
BTC_CACHE_PATH = os.path.join(BASE_DIR, ".btc_cache.json")
|
||
|
||
# ---- reTerminal E1002 设备遥测(SenseCraft HMI,电量/温度/湿度,宽版顶部状态栏)----
|
||
# api-key 由用户在 SenseCraft 后台生成,放 .env;设备每 300s 上报一次,DEVICE_TTL 与之同步(0=每次实拉)。
|
||
SENSECRAFT_API_BASE = os.environ.get("SENSECRAFT_API_BASE", "https://sensecraft-hmi-api.seeed.cc")
|
||
SENSECRAFT_API_KEY = os.environ.get("SENSECRAFT_API_KEY", "")
|
||
SENSECRAFT_DEVICE_EUI = os.environ.get("SENSECRAFT_DEVICE_EUI", "")
|
||
DEVICE_TTL = int(os.environ.get("DEVICE_TTL", "300"))
|
||
DEVICE_CACHE_PATH = os.path.join(BASE_DIR, ".device_cache.json")
|
||
|
||
# ---- AI 用量数据源(Claude Usage / Codex Usage 两个模块)----
|
||
# 形态 A:两路都在**本机**采集(需本机的登录态),由 main.py --target push 上报给 server.py;服务端不持任何凭证、不读本机文件。
|
||
# Claude(claude_usage.py):复用 Claude Code 自身的 OAuth 登录态(macOS 钥匙串「Claude Code-credentials」,只读、绝不刷新),
|
||
# 直接调 https://api.anthropic.com/api/oauth/usage——与 Claude Code `/usage` 同源、**账户级**(CLI/桌面/网页消耗都算),
|
||
# 不依赖 Claude Code 进程在跑、不依赖 statusline 插件。
|
||
CLAUDE_USAGE_TTL = int(os.environ.get("CLAUDE_USAGE_TTL", "300")) # 秒,快照有效期(同 WEATHER_TTL 语义,0=每次实拉);社区一致建议 ≥300(突发调用 429 会封约 24 小时)
|
||
CLAUDE_USAGE_BACKOFF = int(os.environ.get("CLAUDE_USAGE_BACKOFF", "1800")) # 秒;拉取失败后的退避期,期内沿用旧快照
|
||
CLAUDE_USAGE_SCOPE_MODEL = os.environ.get("CLAUDE_USAGE_SCOPE_MODEL", "Fable") # limits[] 里模型限定周额度按 display_name 识别
|
||
CLAUDE_USAGE_SCOPE_FALLBACK_KEY = os.environ.get("CLAUDE_USAGE_SCOPE_FALLBACK_KEY", "seven_day_opus") # 旧版顶层键兜底
|
||
CLAUDE_USAGE_UA = os.environ.get("CLAUDE_USAGE_UA", "claude-code/2.1.34")
|
||
CLAUDE_USAGE_CACHE_PATH = os.path.join(BASE_DIR, ".claude_usage_cache.json") # 只存快照与时间戳,不存凭证
|
||
# Codex(codex_usage.py):经官方 `codex app-server` JSON-RPC 读 ChatGPT 订阅的 Codex 额度池(CLI/IDE/Web/桌面 App 内 Codex 共用);
|
||
# token 刷新由 Codex CLI 代管。需 Codex 以 ChatGPT 账号登录(`codex login`);CODEX_USAGE_HOME 留空用默认 ~/.codex,
|
||
# 也可指向独立 CODEX_HOME 与日常配置隔离。
|
||
def _which_codex():
|
||
"""launchd 不继承登录 shell 的 PATH(无 /opt/homebrew/bin),故按 PATH → Homebrew/常见位置解析出绝对路径。"""
|
||
import shutil
|
||
env = os.environ.get("CODEX_BIN", "")
|
||
if env:
|
||
return os.path.expanduser(env)
|
||
for cand in (shutil.which("codex"), "/opt/homebrew/bin/codex", "/usr/local/bin/codex", os.path.expanduser("~/.npm-global/bin/codex")):
|
||
if cand and os.path.exists(cand):
|
||
return cand
|
||
return "codex"
|
||
CODEX_BIN = _which_codex()
|
||
# 留空 = 用 Codex 默认目录 ~/.codex(2026-09-14 用户直接在默认目录用 ChatGPT 账号登录,故默认跟随之);
|
||
# 若日常 Codex 需保持 API Key/中转登录而另开一份 ChatGPT 登录,可设为独立目录(如 ~/.codex-usage)并在其下 `CODEX_HOME=… codex login`。
|
||
CODEX_USAGE_HOME = os.environ.get("CODEX_USAGE_HOME", "")
|
||
CODEX_USAGE_TTL = int(os.environ.get("CODEX_USAGE_TTL", "300")) # 秒,快照有效期(0=每次实拉)
|
||
CODEX_USAGE_BACKOFF = int(os.environ.get("CODEX_USAGE_BACKOFF", "900"))
|
||
CODEX_USAGE_CACHE_PATH = os.path.join(BASE_DIR, ".codex_usage_cache.json")
|
||
USAGE_WARN = int(os.environ.get("USAGE_WARN", "80")) # 用量 ≥ 此百分比 → 进度条与百分比转红预警(两个版面统一)
|
||
# 以下两项仅六色宽版(E1002)使用,三色小屏只有 黑/红 两档。用量与 pace 各自三档、独立取色,颜色顺序固定为 绿 → 黑 → 红
|
||
# (黄不用:2026-09-14 曾按要求加过 60%~80% 黄档,随即被否——黄小字压白底发虚、且四档在三行里过于花):
|
||
USAGE_OK = int(os.environ.get("USAGE_OK", "30")) # 用量 ≤ OK 绿「健康」|OK~WARN 黑「正常」|≥ WARN 红「危险」;进度条、边框与百分比数字同色
|
||
PACE_TOL = int(os.environ.get("PACE_TOL", "10")) # pace=用量%−时间%:≤ −TOL 绿「节余」|±TOL 内黑「持平」|≥ +TOL 红「偏快」
|