那天逆变器报了个"输出偏低"的告警,运维的师傅在阵列地图上点了一圈,只看到 A07 这个方阵整片飘红,效率 61%。方阵里有 20 块方阵、每块方阵下面又串着一堆组件,到底哪块板子出问题,师傅说"看着都像,看着又都不像"。
这就是 bug 复盘的开头:告警给到了方阵级,没给到组件级。逆变器只知道 A07 整体的发电效率掉下来了,但它分不清是这一片都衰减了,还是某一块热斑把整串电流拖垮了。要定位到具体组串,光看方阵的总效率没用,得往下一层看组串级的电流和电压差异。
方阵是一张行×组串的矩阵
光伏方阵内部是一行行列排布的组串,每块组件都串在某一条上。每个组串是一串串联的组件,串里的电流被最差的那块组件钳住------一块板子被遮挡或者热斑,整串电流都跟着掉。所以定位热斑的入口,是组串电流的横向对比,单看某一块电压看不出名堂:
ts
export interface StringCell {
row: number // 组串所在行
col: number // 组串所在列
current: number // 组串电流,A
voltage: number // 组串电压,V
temp: number // 组件温度,℃
}
export interface ArrayBlock {
id: string // 如 A07
strings: StringCell[][]
}
const blockA07: ArrayBlock = {
id: 'A07',
strings: [
[
{ row: 0, col: 0, current: 9.1, voltage: 780, temp: 47.2 },
{ row: 0, col: 1, current: 8.9, voltage: 774, temp: 47.8 },
{ row: 0, col: 2, current: 9.0, voltage: 782, temp: 46.9 }
],
[
{ row: 1, col: 0, current: 8.8, voltage: 770, temp: 48.1 },
{ row: 1, col: 1, current: 9.0, voltage: 778, temp: 47.5 },
{ row: 1, col: 2, current: 8.9, voltage: 776, temp: 47.9 }
],
[
{ row: 2, col: 0, current: 5.2, voltage: 812, temp: 62.4 },
{ row: 2, col: 1, current: 8.9, voltage: 775, temp: 47.6 },
{ row: 2, col: 2, current: 9.0, voltage: 781, temp: 47.0 }
]
]
}
看到第 2 行第 0 列了吗?电流 5.2A,别的组串都在 8.8~9.1A,电压反而飙到 812V、温度 62.4℃。这正是热斑的典型特征:电流被钳低、局部温度升高。这个特征在方阵总效率里被摊薄了,所以逆变器只能报"偏低",看不到具体哪一串。
效率先按辐照度归一
直接比电流有个前提:当前辐照度得差不多。晴天正午辐照度 842 W/㎡,早上可能只有 300,组串电流自然低,不能把"早上电流低"误判成热斑。先把实测效率折算到标准辐照度下再比:
ts
function normalizedRatio(str: StringCell, irradiance: number): number {
// 以当前辐照度把电流折算到 1000 W/㎡ 标况,再算与额定电流的比值
const ratedCurrent = 9.5
const norm = str.current * (1000 / irradiance)
return norm / ratedCurrent
}
界面里 A07 显示"辐照度 842 W/㎡"、"组件温度 48.6℃"、"实时功率 1.28 MW",这些环境量都得先拿到手,不然定位矩阵会误报。晴天 31℃ 的中午,正常组串折算后应该在 0.92 以上,掉到 0.6 以下就是异常。

阈值是怎么定下来的
定位矩阵要用的几个数,拉成一张表:
| 参数 | 取值 | 定下来的依据 |
|---|---|---|
| 额定电流 | 9.5 A | 铭牌值,折算分母 |
| 电流偏差下限 | -25% | 同行平均低过四分之一才可疑 |
| 温升下限 | +8℃ | 热斑发热的典型量级 |
| 正常折算比值 | ≥ 0.92 | 晴天中午的经验下沿 |
单低电流可能是阴影,单高温可能是散热,两个齐出现才够格。
组串定位矩阵:横向差才是信号
热斑定位的核心是一张差异矩阵。同一方阵里,日照条件对每串几乎一样,所以正常组串的电流应该彼此接近。谁跟同行、同列的邻居差得远,谁就可疑。我按行算电流偏差,超过阈值就标出来:
ts
export interface HotSpotHit {
row: number
col: number
current: number
rowAvg: number
devPct: number // 与同行平均的偏差百分比
tempDelta: number // 与方阵平均温度的差值
}
function locateHotSpot(block: ArrayBlock, irradiance: number): HotSpotHit[] {
const hits: HotSpotHit[] = []
const avgTemp = block.strings.flat().reduce((s, c) => s + c.temp, 0) / block.strings.flat().length
for (let r = 0; r < block.strings.length; r++) {
const row = block.strings[r]
const rowAvg = row.reduce((s, c) => s + c.current, 0) / row.length
for (let c = 0; c < row.length; c++) {
const cell = row[c]
const dev = (cell.current - rowAvg) / rowAvg
if (dev < -0.25 && cell.temp - avgTemp > 8) {
hits.push({
row: r,
col: c,
current: cell.current,
rowAvg,
devPct: Math.round(dev * 100),
tempDelta: Math.round((cell.temp - avgTemp) * 10) / 10
})
}
}
}
return hits
}
两个条件同时满足才报:电流比同行平均低 25% 以上,且温度比方阵平均高 8℃ 以上。只低电流可能只是被遮挡的阴影,只高温可能是散热问题,两个一起出现才是热斑。5.2A 对 8.9A 的偏差是 -41%,温度高 14℃,稳稳命中。
"稳稳命中"不能靠嘴说,写个断言钉住:
ts
function assertLocate(block: ArrayBlock, irradiance: number): void {
const hits = locateHotSpot(block, irradiance)
if (hits.length !== 1) {
throw new Error(`预期 1 条命中,实际 ${hits.length}`)
}
const hit = hits[0]
if (hit.row !== 2 || hit.col !== 0) {
throw new Error(`命中坐标错误:${hit.row}-${hit.col}`)
}
}
阈值或数据一改,断言先炸,省得白跑一趟。
单次低不算数
断言兜得住数据,兜不住采样。云影扫过、鸟粪落脚,都会让某一串电流单次掉一截又回来,这种抖动直接进矩阵,工单能飞上天。同一坐标连续几轮都偏低,才怀疑组件本身。
ts
function isPersistent(history: StringCell[][], row: number, col: number, times: number): boolean {
// 连续 times 次采样都比同行平均低 25% 以上,才算持续性异常
let low = 0
for (const sample of history) {
const cell = sample[row][col]
const rowAvg = sample[row].reduce((s, c) => s + c.current, 0) / sample[row].length
if ((cell.current - rowAvg) / rowAvg < -0.25) {
low++
}
}
return low >= times
}
history 是最近几轮的组串快照,5 分钟一轮,times 取 3,异常得持续 15 分钟以上。热斑不会自己好,云影会。
从命中到工单
定位出来之后,别让结论停在内存里。把命中的组串拼成一条可读的告警,生成工单,让无人机去复检。界面右侧"可能原因:组串 07-3 出现热斑,建议安排无人机复检"就是这么来的:
ts
function buildAlert(hits: HotSpotHit[], blockId: string): string {
if (hits.length === 0) {
return `${blockId} 无异常`
}
const parts = hits.map((h) => `组串 ${h.row + 1}-${h.col + 1}(电流偏差 ${h.devPct}%,温升 ${h.tempDelta}℃)`)
return `${blockId} 疑似热斑:${parts.join('、')},建议安排无人机红外复检`
}
${h.row + 1}-${h.col + 1} 把矩阵坐标翻译成运维师傅听得懂的"07-3"编号,别把 row=2,col=0 这种坐标直接甩给他。这一层翻译很关键,告警最终是要给人看的。
踩坑清单
坑按顺序列一下,每条现象加原因:
- 早上八点方阵电流集体偏低,报了一串疑似热斑。 辐照度没归一,拿 300 W/㎡ 的电流比 842 W/㎡ 的基准,把"天没亮"当"板子坏"。
- 一串电流偶发掉一截又回来,无人机飞过去什么都没看见。 单次采样就下结论,云影的瞬时凹陷没做持续性过滤。
- 结论甩出 row=2,col=0,运维师傅看着屏幕发呆。 矩阵坐标没翻译成组串编号,告警是给人看的,得写"07-3"。
- 多云天热斑差点漏过去。 均值被二十串摊薄,62.4℃ 只高出 14℃,8℃ 的阈值余量不多。
text
SolarField/
├─ entry/src/main/ets/
│ ├─ pages/Index.ets # 阵列热斑图、方阵详情、功率曲线
│ ├─ features/HotSpotMatrix.ets # 组串差异矩阵与热斑定位
│ ├─ features/Normalizer.ets # 辐照度归一折算
│ └─ entryability/EntryAbility.ets
├─ entry/src/main/resources/ # 色板 #2B321F / #7EA443 / #D96C4F
└─ build-profile.json5

屏幕左侧是 20 个方阵的热斑图,A07 被红框选中、效率 61% 标红,其余方阵按 90% 上下显示绿色。右侧是 A07 的详情:实时功率、直流电压、组件温度、辐照度,底部是热斑定位结论和无人机复检建议。
复盘这件事,根因是告警粒度太粗。逆变器只能看到方阵级的总效率,热斑的信息其实藏在组串级的电流差和温度差里。把方阵展开成行×组串矩阵,横向一比,那一串 5.2A、62.4℃ 的板子就自己跳出来了。定位靠的是数据重排,把原本平铺的数据摆成一个能横向比较的形状。
本文描述的是光伏电站热斑定位的数据处理思路,用于演示与辅助运维,不构成电站安全运行规范。真实光伏电站的告警阈值、热斑认定与检修作业需按电站运维规程和电气安全规范执行,涉及高压直流侧的操作须由持证人员完成。