# PoVES 现场采集 / Edge collection

## 中文

这是一套只读运行监测接入，不发送充放电指令。目前没有厂家默认连接，也没有正在运行的真实设备采集。采集需要在现场电脑、工控机或网关部署；关闭浏览器不影响已经部署的采集器。

1. 在个人、企业或 Admin 后台登记电站。管理员给客户登记时，先选择客户的已订阅账户，确认电表编号、额定功率、储能容量与时区。
2. 安装 Python 3.10+ 和依赖：`python3 -m pip install -r requirements.txt`。复制配置模板为 `config.json`，运行 `python3 collector.py --config config.json --init-signing-key`。私钥保存在现场的 `signing_key_path`，终端只输出公钥。将该公钥粘贴到电站接入页面，再由负责人生成专属采集凭证（90 天有效）。把凭证放进现场环境变量 `POVES_COLLECTOR_TOKEN`，不要放进公开网址、CSV、代码仓库或发送给其他客户。轮换会立即停用旧凭证。订阅失效会暂停接收。
3. 安装 Python 3.10+。把 `collector.py` 和配置模板放在同一现场目录，将模板复制为 `config.json`。修改 `source.path` 指向 EMS 自动更新的 CSV。`columns` 可映射厂家列名；源数据单位必须已经是 W、Wh、百分比。
4. 修改 `ingest_url` 为服务器可访问的 `/api/telemetry/ingest` 地址。模板中的 localhost 仅适用于采集器与开发服务器在同一台电脑的情况。远程采集必须使用 HTTPS。
5. 先执行 `python3 collector.py --config config.json --once`。核对后台所属账户、原始记录和 15 分钟汇总，然后执行 `python3 collector.py --config config.json` 持续运行。
6. 生产现场把此命令配置为操作系统服务（开机启动、异常重启），配置日志轮转、磁盘监测及凭证到期维护。当前脚本包含持久化待传队列与重试；不会替你安装系统服务。

### 数据含义

每条 CSV 必须是**已完成的 5 分钟区间**，UTC 起点对齐 00、05、10……分。设备端应使用同步时钟。时间戳必须带 Z 或时区偏移，平台按 UTC 分桶，界面按电站时区显示。

- `sampleId`：稳定且唯一的记录编号，重复上传不得生成新编号。
- `meterId`：必须与该凭证绑定电站的电表完全一致。
- `chargeWh`、`dischargeWh`：该 5 分钟内电表实测的充、放电电量，整数 Wh。可由正确处理复位/翻转后的累计电表差值获得，不能把某一个瞬时功率读数直接当成区间电量。
- `avgPowerW`：该 5 分钟的平均功率，整数 W；放电为正，充电为负。
- `socPct`：区间末电池 SOC，0–100。

模板里的固定日期与示例值只是字段示例，不代表设备测量；接入时应全部替换为真实值。示例电量 100 Wh 对应 5 分钟平均放电功率 1200 W。

平台接收后，使用绑定凭证查出账户和电站，客户端不能指定其他 owner。原始记录与汇总均按账户、电站、时段保存。15 分钟区间采用 [00,15)、[15,30)、[30,45)、[45,60)。每到一条数据就更新所在区间的汇总，第三条到齐后成为完整汇总；无需等有人打开网页。电量相加，平均功率取三个等长区间的均值，SOC 取时间上最后一条。只有 1/2 条的汇总标记不完整；完全未收到的时段不伪造为零。

相同编号/时段与内容的重传会跳过；同编号或时段出现不同内容会拒绝，不覆盖旧记录。允许补传最近 7 天已完成的数据，迟到记录会重新汇总。采集器本地 SQLite 队列在断网后保留待传记录；400/409/422 等数据错误会转为 rejected 等待人工排查，其余连接/授权错误下次重试。可检查本地队列，不应删除原始计量证据。持续超过 7 天的离线记录需要另行归档与核对。

### 通用 HTTP 只读来源

如果 EMS 已提供包含相同字段的五分钟记录数组，可把 source 改成：

```json
{"type":"http_json","url":"https://your-ems.example/intervals","rows_key":"records","bearer_token_env":"EMS_READ_TOKEN"}
```

不使用该示例域名实际连接。若返回顶层数组，省略 `rows_key`。适配器不会轮询瞬时读数来伪造区间电量。接口必须返回滚动窗口以供补传，建议 24 小时以上且小于 5 MB / 5000 行。CSV 推荐通过临时文件写完后原子替换，避免读取半行。脚本读取与上传均不跟随重定向，避免凭证被转发。

### 私有站点访问与下一步

会员权限与站点托管入口的访问限制互相独立。如果托管入口返回 Cloudflare 403，采集凭证无法解除该入口限制。当前私有 Sites 域名不能视作已经可供现场网关访问；需站点所有者明确选择可被采集器访问的部署入口或独立采集网关，再配置同一安全接收逻辑。不要复制浏览器登录 Cookie 到采集器，也不要未经同意公开会员后台。

运行遥测 ≠ PoVES 核验通过 ≠ 铸币。真实 PoVES 仍需要授权请求及 EMS、PCS、BMS、电表交叉证据、签名和核验规则；本采集凭证是上传身份凭证，不是设备计量真实性证明，也不会触发自动发币。

## English

The collector is a **read-only monitoring process** deployed on an on-site computer or gateway. It does not control the battery. No vendor connection or live equipment collection is preconfigured.

Register the station in the correct subscribed account, generate its 90-day credential, and set `POVES_COLLECTOR_TOKEN` on the gateway. Install Python 3.10+ and `python3 -m pip install -r requirements.txt`, copy the config template to `config.json`, set the real CSV source and accessible HTTPS ingestion URL, then run `python3 collector.py --config config.json --once` before starting continuous mode without `--once`. Configure your operating system to start/restart the collector as a service. The localhost template only works when the collector and development server share a computer.

The source must provide completed, UTC-aligned five-minute intervals with stable sample IDs, matching meter ID, measured integer energy in Wh, interval-average power in W (positive discharge), and end-of-interval SOC in percent. Timestamps require Z or an explicit UTC offset. Map source column names in `columns`; convert source units correctly before ingestion. Example CSV rows are illustrative, not measurements. A single instantaneous power reading must never be represented as measured interval energy.

Each upload updates the corresponding UTC quarter-hour bucket transactionally. Three distinct intervals make a complete aggregate: sum measured energy, average equal-duration interval power, and use the latest interval's SOC. Partial buckets remain incomplete; missing intervals are never invented as zero. The dashboard uses the station's timezone. The device credential, not a client-supplied owner field, determines account and station ownership.

Identical retries are idempotent; conflicting data cannot replace stored samples. Completed intervals from the last seven days can be backfilled. The SQLite outbox survives outages and retains pending or rejected records; monitor logs, disk space and token expiration. Source exports should be rolling windows, at most 5 MB / 5000 rows, written atomically. Generic HTTP JSON interval arrays are also supported with `source.type=http_json`, `url`, optional `rows_key`, and an optional source-token environment variable. Redirects are disabled to avoid forwarding credentials.

Site-level access and collector authorization are separate. A Cloudflare 403 at the private hosting gateway cannot be resolved with a collector token. A reachable, authorized ingestion deployment or dedicated gateway must be arranged before real devices can upload. Do not copy browser cookies or publish the member dashboard as a workaround. Subscriptions expiring or being suspended stop ingestion; revoked or rotated credentials stop working.

Telemetry is not a verified PoVES service proof and does not trigger minting. Cross-source evidence, device signatures and service validation remain separate.
# 运营中心与异常管家（采集器 v2）

采集器每 5 分钟重新读取配置的数据源，补传本地队列，并向同域名的 `/api/operations/agent` 汇报心跳、待传数量、拒收数量和数据源异常。心跳使用原有电站采集凭证，订阅到期、凭证到期或撤销后停止被服务器接受。界面关闭不影响正在运行的采集器；电脑关机或进程退出后采集停止。

同一电表、同一时段的更正数据被服务器确认后，本地旧拒收记录会标记为 `superseded`，不再计入待核对拒收数量，但原文仍保留。其他时段的拒收记录不会因此清除。

网络故障时数据保留在本地 SQLite 队列，下一轮自动重试。相同原始记录只入队一次；服务器按账户、电站和时段去重，冲突数据不会覆盖已有读数。拒收数据保留供核对，不能通过重试强行覆盖。更正源文件后，新内容会重新入队。超过服务器 7 天接收窗口的数据需另行核对，不会伪造或补零。

运营中心的“补扫数据源”会下发只读重扫请求。采集器下一轮重新读取现有配置中的滚动文件或 HTTP 数据，再报告“扫描完成”；这只代表重扫已执行，数据是否补齐以缺口数量为准。源文件须保留需补传的历史记录（最多 7 天、5000 行）。系统不会凭空取得 EMS 未提供的历史数据，也不会远程控制储能设备。

缺口检查覆盖近 24 小时，跳过电站接入前时段，预留 5 分钟上传宽限；15 分钟无心跳显示离线。数据恢复后，对应告警会自动恢复。源文件存在无效行会显示数据源异常。下载更新后的 `collector.py` 并重启现场采集器才能使用心跳与补扫功能。部署域名还须允许采集器正常访问；平台级访问拦截不会由采集凭证自动解除。

## Signed requests / 签名上报升级

旧版无签名采集请求会被拒绝。先备份现场待传队列，安装本目录依赖，运行 `python3 collector.py --config config.json --init-signing-key`，由账户负责人将输出的 Ed25519 公钥绑定到新凭证，再重启采集器。初始化不会覆盖已有私钥；轮换签名密钥时使用新的文件路径。不要将私钥上传到平台、写入 CSV 或发给维护人员。Unix 私钥文件必须为 0600；现场磁盘加密及系统权限仍由站点配置。

每次上报与心跳使用独立签名，绑定接口、原始正文摘要、发送时间和随机请求编号。时间误差最多 5 分钟；旧数据补传仍受原有 7 天窗口限制。已使用的请求编号不可重放；队列重试会生成新签名，原始时段仍按内容去重。每凭证每分钟最多 120 次请求；限流时保留待传队列，下一轮继续。采集签名证明持钥方提交过内容，不等于电表校准、硬件防篡改或服务核验通过。

Unsigned legacy requests are rejected. Install the requirements and run `python3 collector.py --config config.json --init-signing-key` to generate a local Ed25519 private key. Only the public key is printed; paste it into the station connection screen before an owner issues a replacement credential. Initialization never overwrites an existing private key. Set `signing_key_path` to a new file to rotate device identity. Keep the private file on the gateway, owner-readable only (0600 on Unix).

Both telemetry and heartbeats sign the endpoint, raw body digest, current timestamp and a fresh nonce. Requests tolerate five minutes of clock skew; historical sample backfill remains limited to seven days. Replayed nonces are rejected; retries create a fresh signature while sample deduplication prevents double counting. The server limits each credential to 120 requests per minute. Rate limiting preserves the local pending queue. A software signature establishes possession of the registered key, not physical metering truth.
