总用电下降了,为什么家庭能源看板仍然提示异常

看板左上角的总功率是 3.8 kW,比昨天同一时刻低了 8%,绿色的趋势线看着很舒服。可往下看,厨房区域的卡片是黄的,上面标着"洗碗机运行中"。

用户的疑问很合理:总量都下降了,还有什么好提示的?

把采样翻出来才看明白。昨天这个点是 4.1 kW,其中客厅空调占了 1.5 kW;今天空调没开,客厅掉到 1.2 kW。省下来的电被厨房吃掉了------洗碗机今天在同一时段启动了,功率 0.8 kW。总量因为这个此消彼长下降了,但厨房确实出现了一台平时不该出现在这个时段的大功率设备。

所以"总量下降"和"每个区域正常"是两件事。看板的异常提示盯的是区域和设备的偏离,跟总量没什么关系。

功率要按来源拆开,不能只记一个总数

第一版的采样只存了一个总功率。这个数据结构没法回答"今天的下降是真的省电,还是光伏出力变大了"。

光伏和储能接进来之后,同一时刻家里有三路能量在动:电网取电、光伏自用、电池放电或充电。总负荷是三者叠加的结果:

ts 复制代码
export interface RoomLoad {
  roomId: string
  watts: number
}

export interface PowerSample {
  capturedAt: number
  gridW: number        // 从电网取电,正数表示消耗
  solarW: number       // 光伏自用部分,恒为正
  batteryW: number     // 正数为充电,负数为放电
  rooms: RoomLoad[]
}

export function homeLoad(sample: PowerSample): number {
  // 电池充电时这部分不算家庭负荷,所以取负号把它还原成负荷口径
  return sample.gridW + sample.solarW - sample.batteryW
}

batteryW 的符号约定必须在项目里统一。电池充电时它记正数,家庭负荷要减掉这部分,因为它没有真正被用掉;放电时记负数,减掉负数等于加回来,正好把电池供给的那部分算进负荷。这个符号在早期版本里被两个模块用了不同的约定,界面上出现过"总负荷比电网取电还低"的显示。

界面上的"光伏供给 62%"来自另一组计算:

ts 复制代码
export function solarShare(sample: PowerSample): number {
  const load: number = homeLoad(sample)
  if (load <= 0) {
    return 0
  }
  const share: number = (sample.solarW / load) * 100
  // 光伏可能向电网反送,这里只表示自用占比,上限取 100
  return Math.min(100, share)
}

基线不能用昨天,要用同一时段的长期分布

最早的异常判定很直觉:拿今天和昨天比,差得多了就报。这个做法在住家里几乎不可用。洗衣服、做饭、洗澡都有星期规律,工作日和周末的差异能到两三倍。用昨天当基线,周二的用电在周一面前永远是异常。

改成按"星期几 + 小时"建槽位,一周 168 个槽:

ts 复制代码
export interface BaselineSlot {
  slotIndex: number    // 0 到 167,等于 星期数 * 24 + 小时
  meanW: number        // 该槽位的历史均值
  stdW: number         // 标准差
  samples: number      // 参与统计的样本数
}

export interface AnomalyVerdict {
  abnormal: boolean
  reason: string
  deviationW: number
}

const MIN_SAMPLES: number = 4
const Z_THRESHOLD: number = 2.5
const MIN_ABSOLUTE_W: number = 300

export function detectAnomaly(slot: BaselineSlot, actualW: number): AnomalyVerdict {
  const deviation: number = actualW - slot.meanW

  // 样本太少就不判定,避免刚装上的设备天天被标异常
  if (slot.samples < MIN_SAMPLES) {
    return { abnormal: false, reason: '基线样本不足', deviationW: deviation }
  }

  // 绝对偏离太小就不报,小功率设备的相对波动没有意义
  if (Math.abs(deviation) < MIN_ABSOLUTE_W) {
    return { abnormal: false, reason: '偏离在正常范围内', deviationW: deviation }
  }

  const safeStd: number = slot.stdW < 1 ? 1 : slot.stdW
  const z: number = Math.abs(deviation) / safeStd

  if (z < Z_THRESHOLD) {
    return { abnormal: false, reason: '波动未超过历史范围', deviationW: deviation }
  }

  return {
    abnormal: true,
    reason: deviation > 0 ? '高于同时段基线' : '低于同时段基线',
    deviationW: deviation
  }
}

三个条件必须同时满足才算异常,缺一个都会产生误报。

样本量门槛来自实际体验。新设备接入后的头两天,基线里只有两三个样本,某个样本恰好是待机状态,均值被拉得很低,之后设备正常工作就被一直判定为"高于基线 400%"。加了这个门槛,头两天安静地学习,第三天开始才给出提示。

绝对偏离门槛解决的是小功率设备的问题。路由器平时 8 W,某天因为固件升级跑到 12 W,相对变化 50%,但图上看不出任何意义。300 W 这个值是根据家里几个主要电器的实际波动定的。

标准差取了下限 1 W。历史样本全都一样的时候标准差会算出 0,除法直接炸掉。

Z_THRESHOLD 定在 2.5 而不是常见的 1.96,是有意调松的。1.96 对应 95% 置信,家用场景下一天下来会报出五到八条提示,用户很快就学会无视它们。2.5 大约对应 99%,实测一周只会报出两三条真正值得看的。

分区判定要独立跑一遍

有了区域和设备的功率,就能解释为什么总量下降还会报异常。判定是逐个区域独立做的,不看总量:

ts 复制代码
export interface RoomVerdict {
  roomId: string
  abnormal: boolean
  reason: string
  deviationW: number
}

export function judgeRooms(
  rooms: RoomLoad[],
  baselines: Map<string, BaselineSlot>,
  now: number
): RoomVerdict[] {
  const slotIndex: number = slotOf(now)

  return rooms.map((room: RoomLoad) => {
    const slot: BaselineSlot | undefined = baselines.get(room.roomId)
    if (slot === undefined) {
      return { roomId: room.roomId, abnormal: false, reason: '暂无基线', deviationW: 0 }
    }

    const shifted: BaselineSlot = { ...slot, slotIndex }
    const verdict: AnomalyVerdict = detectAnomaly(shifted, room.watts)

    return {
      roomId: room.roomId,
      abnormal: verdict.abnormal,
      reason: verdict.reason,
      deviationW: verdict.deviationW
    }
  })
}

一个区域大幅下降、另一个区域大幅上升,两者的偏离在总量里互相抵消,可分区判定各自都能看出来。这正是"总用电下降了却还在提示异常"的成因。

界面上客厅卡片显示"较昨日下降 8%",用的是环比;它的异常判定用的是同时段基线。两个数字不一样是正常的,缺一个都会让用户困惑:只给环比,解释不了为什么降了还报警;只给基线偏离,用户又不知道跟自己印象里的昨天差多少。

目录结构

text 复制代码
EnergyHub/
├─ entry/src/main/ets/
│  ├─ pages/Index.ets                     # 能源流向与区域负荷卡片
│  ├─ features/
│  │  ├─ PowerModel.ets                   # 采样、负荷与基线结构
│  │  ├─ BaselineLearner.ets              # 168 槽位基线的累积
│  │  └─ AnomalyJudge.ets                 # 偏离判定与分区归因
│  ├─ entryability/EntryAbility.ets
│  └─ adapters/
│     ├─ MeterAdapter.ets                  # 电表与光伏逆变器读数
│     └─ BatteryAdapter.ets                # 储能状态
├─ entry/src/main/resources/
└─ build-profile.json5

BaselineLearner 每天凌晨用前一天的数据更新一次槽位,白天只读。基线如果在白天滚动更新,异常判定会被自己刚才的判定结果带偏------判定为异常的高功率采样如果也进了基线,这条基线会慢慢爬高,之后同样的用电就不报警了。

界面上的"今日节省 ¥8.40"也是估算值。它用当日自用光伏量乘以当地电价得到,在分时电价地区这个数字只能算个大概。真要准确结算,得引入分时电价表和上网电价,那是另一套逻辑,不该混在实时看板里算。

数据只在本机处理,不上传云端,历史采样按天滚动保留最近 90 天。这些设置都放在系统设置页,用户可以单独关掉某个区域的异常提示。

相关推荐
xiaogai_gai1 小时前
电商采购入库对接财务VMI采购入库(单号回写)实战教程
大数据·数据库
jerryinwuhan1 小时前
离散元分析(DEM)和机器学习(Machine Learning, ML)
人工智能·机器学习
程序猿乐锅1 小时前
【从0-1学习Python | 第四篇】Python 字符串和元组
java·开发语言·笔记·后端·python·学习
拆房老料1 小时前
【第四部分:Agentic 智能体设计模式】15.为什么 Agent 也需要设计模式
人工智能·设计模式·ai·ai-native
开开心心就好1 小时前
Windows视频播放器推荐,精简版体积更小
java·服务器·开发语言·eclipse·pdf·ocr·音视频
开开心心就好1 小时前
A3的PDF怎么切成A4?拖一下分割线就行
服务器·开发语言·python·算法·pdf·ocr·tornado
Yan卌1 小时前
探析全球虫害治理前沿 —— 美国 PestWorld 展会简述
大数据·运维·人工智能
HR研习社1 小时前
绚星AI实战训练场:不只是培训平台里的一个功能
人工智能·ai陪练·企业ai陪练·企业培训平台
小黄人软件1 小时前
python 模块(Module)与包(Package)的区别,python xxx.py、python -m xxx 和 import xxx 三者的区别
开发语言·python