光储充系统数据流全解析:PV / ESS / GRID 数据怎么流转,线损怎么算
做过能源类项目的前端,大概率都遇到过这个场景:后端扔过来一堆实时功率数据,字段名是 pv_power、ess_power、grid_power、load_power,让你展示到大屏上。
但这些数字背后的逻辑是什么?数据怎么来的?正负号代表什么?线损怎么算?异常数据怎么处理?
这篇文章从前端视角把这条数据链路完整梳理一遍。
一、光储充系统是什么
先把业务背景说清楚,不然后面的数据字段完全看不懂。
光储充 = 光伏(PV)+ 储能(ESS)+ 充电桩(Charge),是目前园区、工厂、停车场最常见的综合能源系统。

四个核心节点:
- PV(光伏):发电侧,白天发电,功率随光照变化
- ESS(储能):缓冲侧,谷时充电,峰时放电,平衡供需
- GRID(电网):兜底侧,本地电不够时从电网买,多了往电网卖
- LOAD(负载):消耗侧,园区所有用电设备的总和
充电桩在系统里属于负载的一部分,但因为功率大、波动频繁,通常单独采集。
二、数据怎么来的
采集链路
设备侧的数据采集链路一般是这样的:
电表/逆变器/BMS
→ 采集网关(Modbus RTU / Modbus TCP)
→ 边缘计算节点
→ MQTT Broker
→ 后端服务
→ 前端(WebSocket 推送 或 HTTP 轮询)

前端通常只跟最后一段打交道:WebSocket 实时推送 或 HTTP 定时轮询。
典型数据结构
后端推过来的实时数据大概长这样:
json
{
"timestamp": 1719820800000,
"pv_power": 215.95,
"ess_power": -12.45,
"grid_power": 448.85,
"load_power": 652.35,
"charge_power": 86.50,
"soc": 78.5,
"grid_frequency": 50.02,
"grid_voltage": 10.00
}
单位统一是 kW,时间戳是毫秒级 Unix 时间戳。
三、正负号代表什么
这是前端最容易搞错的地方,正负号不是数据异常,是方向定义。
ESS 的正负
ini
ess_power > 0 → 储能放电(向外输出能量)
ess_power < 0 → 储能充电(从外部吸收能量)
ess_power = 0 → 待机状态
上面数据里 ess_power: -12.45,表示储能正在以 12.45 kW 的功率充电。
GRID 的正负
grid_power > 0 → 从电网购电(用电大于本地发电)
grid_power < 0 → 向电网售电(本地发电大于用电)
PV 和 LOAD
光伏只发电不消耗,始终 >= 0。 负载只消耗不发电,始终 >= 0。
💡 前端展示时要注意:ESS 和 GRID 的正负号需要转成用户能看懂的语言,不能直接把
-12.45显示出来。应该显示为"充电中 12.45 kW"或者"放电中",配合不同颜色区分状态。
四、功率平衡公式
光储充系统有一个核心约束,在任意时刻都必须成立:
ini
PV + ESS + GRID = LOAD
用上面的数据验证一下:
ini
215.95 + (-12.45) + 448.85 = 652.35 ✓
这个公式是系统能量守恒的体现,前端可以用它做数据合法性校验:
javascript
function validatePowerBalance(data) {
const { pv_power, ess_power, grid_power, load_power } = data
const calculated = pv_power + ess_power + grid_power
const diff = Math.abs(calculated - load_power)
// 允许 ±2kW 的误差(传感器精度)
if (diff > 2) {
console.warn(`功率不平衡,偏差 ${diff.toFixed(2)} kW,数据可能异常`)
}
}
五、线损怎么算
什么是线损
线损是电能在传输过程中的损耗,主要来自线路电阻发热。在光储充系统里,线损 = 理论发电量 - 实际用电量之间的差值。
两种常见公式
公式一:基于功率节点计算
线损 = PV + 电网输入 - 负载总消耗
适用于系统边界清晰、有独立计量的场景:
javascript
function calcLineLoss(pv, grid, load) {
// grid 为正表示购电,为负表示售电
const input = pv + Math.max(grid, 0) // 总输入
const output = load + Math.abs(Math.min(grid, 0)) // 总输出(含售电)
return input - output
}
公式二:基于关口表和负荷表
线损 = PV发电量 + 关口表示数 - 负荷表示数
适用于有独立安装电表的场景,精度更高,是电力公司常用的统计口径:
javascript
function calcLineLossFromMeter(pvEnergy, gateMeterEnergy, loadMeterEnergy) {
return pvEnergy + gateMeterEnergy - loadMeterEnergy
}

线损率
javascript
function calcLineLossRate(lineLoss, totalInput) {
if (totalInput <= 0) return 0
return ((lineLoss / totalInput) * 100).toFixed(2)
}
// 线损率正常范围一般在 2%~8%
// 超过 10% 通常说明有异常(线路老化、设备故障、数据采集问题)
六、前端怎么处理这些数据
实时数据接入
javascript
// WebSocket 接入示例
class EnergyDataWS {
constructor(url) {
this.url = url
this.ws = null
this.reconnectTimer = null
}
connect() {
this.ws = new WebSocket(this.url)
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data)
this.handleData(data)
}
this.ws.onclose = () => {
// 断线自动重连,5秒后重试
this.reconnectTimer = setTimeout(() => this.connect(), 5000)
}
}
handleData(data) {
// 1. 数据合法性校验
validatePowerBalance(data)
// 2. 方向转换
const processed = {
...data,
ess_status: data.ess_power > 0 ? 'discharging' : data.ess_power < 0 ? 'charging' : 'standby',
grid_status: data.grid_power > 0 ? 'buying' : data.grid_power < 0 ? 'selling' : 'balanced',
line_loss: calcLineLoss(data.pv_power, data.grid_power, data.load_power)
}
// 3. 推送到状态管理(React useState / Zustand 等)
this.onData(processed)
}
destroy() {
clearTimeout(this.reconnectTimer)
this.ws?.close()
}
}
异常数据处理
实际项目里会遇到几种典型异常:
javascript
function sanitizeData(data) {
return {
// PV 不可能为负,负值说明采集异常
pv_power: Math.max(0, data.pv_power ?? 0),
// SOC 范围 0-100
soc: Math.min(100, Math.max(0, data.soc ?? 0)),
// 功率值超出设备额定值的 1.2 倍,标记为异常
load_power: data.load_power > RATED_LOAD * 1.2
? null // null 表示数据异常,前端显示 "--"
: data.load_power,
// 时间戳超过 30 秒没更新,标记为离线
is_online: Date.now() - data.timestamp < 30000
}
}
ECharts 实时功率曲线
javascript
// 24小时功率趋势图配置
const option = {
color: ['#3A86FF', '#FF9A3C', '#8B7FFF', '#34C98A'],
legend: {
data: ['光伏功率', '负载功率', '电网功率', '储能功率']
},
xAxis: {
type: 'time',
axisLabel: {
formatter: (val) => dayjs(val).format('HH:mm')
}
},
yAxis: {
type: 'value',
name: 'kW',
// 允许负值(储能充电/电网售电)
min: 'dataMin'
},
series: [
{
name: '光伏功率',
type: 'line',
smooth: true,
areaStyle: { opacity: 0.15 },
data: pvData
},
{
name: '储能功率',
type: 'line',
smooth: true,
// 储能有正负,用虚线区分
lineStyle: { type: 'dashed' },
markLine: {
data: [{ yAxis: 0 }], // 零轴参考线
lineStyle: { color: '#666' }
},
data: essData
}
// 其余系列类似
]
}
七、今日能量分布怎么统计
功率是瞬时值(kW),能量是累积值(kWh),需要对功率做时间积分:
javascript
// 梯形积分法,精度够用
function calcEnergy(powerSeries) {
// powerSeries: [{ timestamp, value }, ...]
let energy = 0
for (let i = 1; i < powerSeries.length; i++) {
const dt = (powerSeries[i].timestamp - powerSeries[i-1].timestamp) / 3600000 // 转换为小时
const avgPower = (powerSeries[i].value + powerSeries[i-1].value) / 2
energy += avgPower * dt
}
return energy // kWh
}
// 今日能量分布
const todayEnergy = {
pv: calcEnergy(pvPowerSeries), // 光伏发电量
grid: calcEnergy(gridPowerSeries), // 电网购电量(只取正值部分)
charge: calcEnergy(chargePowerSeries), // 充电桩用电量
load: calcEnergy(loadPowerSeries), // 总负载用电量
}
八、数据流完整示意

css
设备采集
↓ MQTT
后端处理
↓ WebSocket
前端接收
↓
数据校验(功率平衡检验)
↓
方向解析(ESS正负 / GRID正负)
↓
衍生计算(线损 / 能量积分 / 线损率)
↓
状态管理(Zustand / Redux)
↓
UI渲染(ECharts / 仪表盘 / 数值卡片)
小结
光储充数据流的核心逻辑就这几条:
- 正负号是方向,不是异常,ESS 和 GRID 都有充放双向
- 功率平衡公式
PV + ESS + GRID = LOAD可以用来做数据校验 - 线损有两种算法,关口表方式精度更高,节点计算方式更实时
- 功率→能量 需要时间积分,前端自己算用梯形积分就够用
- 异常数据 要在进入渲染之前做清洗,不能让 null 或负值直接上图表
这套逻辑在光储充、微电网、楼宇能源管理里都适用,字段名可能不同,但数据流转的本质是一样的。
