光储充系统数据流全解析:PV / ESS / GRID 数据怎么流转,线损怎么算

光储充系统数据流全解析:PV / ESS / GRID 数据怎么流转,线损怎么算

做过能源类项目的前端,大概率都遇到过这个场景:后端扔过来一堆实时功率数据,字段名是 pv_poweress_powergrid_powerload_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 或负值直接上图表

这套逻辑在光储充、微电网、楼宇能源管理里都适用,字段名可能不同,但数据流转的本质是一样的。

相关推荐
古茗前端团队3 小时前
急招!前端|测试|后端|产品(名额多,速来)
前端·后端·架构
Lsx_3 小时前
不只是 Prompt:用 Superpowers Skill 给 AI 编程装上工程化工作流
前端·ai编程·claude
小碗细面4 小时前
前端 Prompt 工程实战:如何搭建场景化 Prompt 库
前端·ai编程
阿瑞IT4 小时前
2026年 AI Agent 生产化落地全景:四大高频故障根因分析与工程解法
前端
木木剑光4 小时前
我开源了一个 React 组件库,沉淀了多个高频组件和实用 Hooks
前端·javascript·react.js
kyriewen4 小时前
DeepSeek API 高峰时段涨价 2 倍,便宜大碗的时代要结束了?
前端·ai编程·deepseek
Moment4 小时前
牛逼,NextJs 从 16.3 开始全面拥抱 Agent Native 🥰🥰🥰
前端·后端·面试
沸点小助手5 小时前
6月沸点活动获奖名单公示|本周互动话题上新🎊
前端·后端
Csvn5 小时前
React 19 `use()` 来了:以后数据加载可以不用 useEffect?
前端·react.js