Files
dorm-power-monitor/go/history.go
fallensigh 57b63316f3 feat: 精确耗电计算(接入充值记录接口)
- 新增 queryISIMSRoomBuyRecord 充值记录获取 + 10分钟缓存

- 日耗电 = 剩余变化 + 当天充值电量(电价0.535元/度)

- 月度汇总增加充值次数/电量/金额

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-23 21:43:30 +08:00

272 lines
6.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"os"
"sort"
"strings"
"sync"
"time"
)
// history.go - 每日电费记录存储JSON 文件)与耗电统计
type record struct {
Date string `json:"date"` // YYYY-MM-DD
Surplus float64 `json:"surplus"`
Amount float64 `json:"amount"`
Source string `json:"source"` // auto / manual
CreatedAt int64 `json:"created_at"`
}
type dailyStat struct {
Date string `json:"date"`
Surplus float64 `json:"surplus"`
Amount float64 `json:"amount"`
Usage *float64 `json:"usage"` // nil 表示无前一天基线
Recharge *float64 `json:"recharge"` // 当天充值电量(度), nil=无充值
Money *float64 `json:"money"` // 当天充值金额(元), nil=无充值
}
// monthSummary 月度汇总
type monthSummary struct {
Month string `json:"month"` // YYYY-MM
RecordCount int `json:"record_count"` // 记录天数
StartSurplus *float64 `json:"start_surplus"` // 月初首次电量
EndSurplus *float64 `json:"end_surplus"` // 月末末次电量
TotalUsage *float64 `json:"total_usage"` // 月总耗电
AvgUsage *float64 `json:"avg_usage"` // 日均耗电
RechargeDays int `json:"recharge_days"` // 充值天数
RechargeKwh *float64 `json:"recharge_kwh"` // 月充值电量(度)
RechargeMoney *float64 `json:"recharge_money"` // 月充值金额(元)
MaxUsage *float64 `json:"max_usage"` // 单日最大耗电
}
var (
historyPath = envOr("HISTORY_FILE", "data/electric_history.json")
historyLock sync.Mutex
historyLoaded bool
historyCache []record
)
// loadHistory 读取历史记录
func loadHistory() []record {
historyLock.Lock()
defer historyLock.Unlock()
if historyLoaded {
return historyCache
}
b, err := os.ReadFile(historyPath)
if err != nil {
historyLoaded = true
return nil
}
_ = json.Unmarshal(b, &historyCache)
historyLoaded = true
return historyCache
}
// saveHistory 持久化历史记录
func saveHistory(records []record) error {
historyLock.Lock()
defer historyLock.Unlock()
if err := os.MkdirAll(dirName(historyPath), 0o755); err != nil {
return err
}
b, _ := json.MarshalIndent(records, "", " ")
if err := os.WriteFile(historyPath, b, 0o644); err != nil {
return err
}
historyCache = records
return nil
}
// hasRecord 判断指定日期是否已有记录
func hasRecord(dateStr string) bool {
for _, r := range loadHistory() {
if r.Date == dateStr {
return true
}
}
return false
}
// addRecord 写入(或覆盖)一条记录
func addRecord(dateStr string, surplus, amount float64, source string) error {
records := loadHistory()
updated := false
for i, r := range records {
if r.Date == dateStr {
records[i] = record{Date: dateStr, Surplus: surplus, Amount: amount, Source: source, CreatedAt: time.Now().Unix()}
updated = true
break
}
}
if !updated {
records = append(records, record{Date: dateStr, Surplus: surplus, Amount: amount, Source: source, CreatedAt: time.Now().Unix()})
}
return saveHistory(records)
}
// getRecords 返回降序(新→旧)记录列表
func getRecords() []record {
records := loadHistory()
sort.Slice(records, func(i, j int) bool {
return records[i].Date > records[j].Date
})
return records
}
// buildDailyStats 按日期升序生成每日统计(含日耗电 = 前一天剩余 - 当天剩余 + 当天充值电量)
// 注意: 耗电计算跨月时使用上月末记录作为基线(若存在)
// rechargeMap: date -> 当天充值电量(度);精确修正充值当天的耗电
func buildDailyStats(records []record, month string, rechargeMap map[string]float64) []dailyStat {
all := loadHistory()
// 若指定月份,额外包含上一月最后一条记录作为基线(不计入本月输出)
var prevMonthLast *record
if month != "" {
prevPrefix := prevMonth(month) + "-"
var last *record
for i := range all {
if strings.HasPrefix(all[i].Date, prevPrefix) {
if last == nil || all[i].Date > last.Date {
last = &all[i]
}
}
}
prevMonthLast = last
}
// 筛选目标记录
var target []record
if month != "" {
target = filterByMonth(records, month)
} else {
target = records
}
sort.Slice(target, func(i, j int) bool {
return target[i].Date < target[j].Date
})
var daily []dailyStat
var prevSurplus *float64
if prevMonthLast != nil {
prevSurplus = &prevMonthLast.Surplus
}
for _, r := range target {
var usage *float64
if prevSurplus != nil {
// 精确耗电 = 剩余变化 + 当天充值电量
u := round2(*prevSurplus - r.Surplus + rechargeMap[r.Date])
if u < 0 {
u = 0 // 极端异常(数据缺口)归零
}
usage = &u
}
var recharge, money *float64
if kwh, ok := rechargeMap[r.Date]; ok && kwh > 0 {
recharge = &kwh
m := round2(kwh * unitPrice())
money = &m
}
daily = append(daily, dailyStat{
Date: r.Date, Surplus: r.Surplus, Amount: r.Amount,
Usage: usage, Recharge: recharge, Money: money,
})
s := r.Surplus
prevSurplus = &s
}
return daily
}
// prevMonth 返回上个月的 YYYY-MM
func prevMonth(month string) string {
t, err := time.Parse("2006-01", month)
if err != nil {
return ""
}
return t.AddDate(0, -1, 0).Format("2006-01")
}
// filterByMonth 按月份(YYYY-MM)筛选记录
func filterByMonth(records []record, month string) []record {
if month == "" {
return records
}
prefix := month + "-"
var out []record
for _, r := range records {
if strings.HasPrefix(r.Date, prefix) {
out = append(out, r)
}
}
return out
}
// buildMonthSummary 生成某月的月度汇总
func buildMonthSummary(daily []dailyStat, month string) *monthSummary {
if len(daily) == 0 {
return &monthSummary{Month: month, RecordCount: 0}
}
// daily 已是升序
m := month
if m == "" {
m = daily[0].Date[:7]
}
start := daily[0].Surplus
end := daily[len(daily)-1].Surplus
var total float64
var max float64
var totalRechargeKwh float64
var totalRechargeMoney float64
rechargeDays := 0
countWithUsage := 0
for _, d := range daily {
if d.Usage != nil {
total += *d.Usage
if *d.Usage > max {
max = *d.Usage
}
countWithUsage++
}
if d.Recharge != nil {
totalRechargeKwh += *d.Recharge
rechargeDays++
}
if d.Money != nil {
totalRechargeMoney += *d.Money
}
}
var totalP, avgP, maxP, startP, endP, rkwhP, rmoneyP *float64
startP, endP = &start, &end
totalP = &total
if countWithUsage > 0 {
avg := round2(total / float64(countWithUsage))
avgP = &avg
}
maxP = &max
if totalRechargeKwh > 0 {
rkwhP = &totalRechargeKwh
}
if totalRechargeMoney > 0 {
rmoneyP = &totalRechargeMoney
}
return &monthSummary{
Month: m,
RecordCount: len(daily),
StartSurplus: startP,
EndSurplus: endP,
TotalUsage: totalP,
AvgUsage: avgP,
RechargeDays: rechargeDays,
RechargeKwh: rkwhP,
RechargeMoney: rmoneyP,
MaxUsage: maxP,
}
}